AQS與ReentrantLock底層源碼分析

聲明:本文爲作者原創,如若轉發,請指明轉發地址

1、AQS底層原理

java併發包下很多API都是基於AQS來實現的,AQS是java併發包的基礎類。裏面維護者一個同步狀態state,和一個同步隊列FIFO以及操作state和同步隊列的方法。

1、同步狀態state:

AbstractQueuedSynchronizer維護了一個state變量,來表示同步器的狀態,state可以稱爲AQS的靈魂,基於AQS實現的好多JUC工具,都是通過操作state來實現的,state=0表示沒有任何線程持有鎖;state=1表示某一個線程拿到了一次鎖,state=n(n > 1),表示這個線程獲取了n次這把鎖,用來表達所謂的“可重入鎖”的概念。

2、FIFO雙向隊列:

Node結點:作爲獲取鎖失敗線程的包裝類, 組合了Thread引用, 實現爲FIFO雙向隊列。 下圖爲Node結點的屬性描述:

img

同步隊列的實現原理:

在這裏插入圖片描述

鏈表初始化的頭節點其實是一個虛擬節點,英文名稱之爲dummy header, 因爲它不會像後繼節點一樣真實的存放線程,並且這個節點只會在真正產生競爭排隊情況下才會延遲初始化,避免性能浪費。
AbstractQueuedSynchronizer 類是一個模版類,維護着這個同步隊列(雙向鏈表),提供着同步隊列一些操作的公共方法,JUC併發包裏基於此類實現了很多常用的併發工具類,如 Semaphore, CountDownLatch等。

同步隊列結構:
在這裏插入圖片描述

2、手寫AQS與ReentrantLock

在這裏插入圖片描述
ReentrantLock內部包含了一個AQS對象,也就是AbstractQueuedSynchronizer類型的對象。這個AQS對象就是ReentrantLock可以實現加鎖和釋放鎖的關鍵性的核心組件。

把AQS當成一個基礎框架,而這個框架提供了一些方法給你,你可以依靠這個框架來實現一個獨佔鎖或者共享鎖,然後再看提供出來的方法的功能,進而進行調用即可。

@Slf4j(topic = "c.TestAQS")
public class TestAQS {
    public static void main(String[] args) {
        MyLock lock = new MyLock();
        new Thread(() -> {
            lock.lock();
            try {
                log.debug("locking...");
                sleep(1);
            } finally {
                log.debug("unlocking...");
                lock.unlock();
            }
        },"t1").start();

        new Thread(() -> {
            lock.lock();
            try {
                log.debug("locking...");
            } finally {
                log.debug("unlocking...");
                lock.unlock();
            }
        },"t2").start();
    }
}

class MyLock implements Lock {
    /**
     * 同步器類
     *
     * 在Lock中維護了一個內部類對象MySync extends AbstractQueuedSynchronizer
     * ReentrantLock內部包含了一個AQS對象,也就是AbstractQueuedSynchronizer類型的對象。
     * 這個AQS對象就是ReentrantLock可以實現加鎖和釋放鎖的關鍵性的核心組件。
     *
     */
    class MySync extends AbstractQueuedSynchronizer{
        /**
         * 嘗試獲取鎖
         */
        protected boolean tryAcquire(int arg){
            //CAS操作將status從0改成1
            if(compareAndSetState(0,1)){
                //加鎖,並設置owner爲當前線程
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        /**
         * 嘗試釋放鎖
         */
        protected boolean tryRelease(int arg){
            //將owner線程設置爲null
            setExclusiveOwnerThread(null);
            //將同步狀態設置爲0
            setState(0);
            //因爲status是volatile的,這個關鍵字可以保證在它之前的操作都會同步到主存中對其他線程可見
            //因此把setExclusiveOwnerThread(null)放到 setState(0)前面
            return true;
        }

        /**
         * 是否持有獨佔鎖
         */
        public boolean isHeldExclusively(){
            //如果status的狀態爲1,說明持有獨佔鎖
            return getState()==1;
        }

        /**
         * 返回一個條件變量
         */
        public Condition newCondition(){
            return new ConditionObject();
        }
    }


    /**
     * 下面的這些抽象方法如果都要自己實現太難了,但是還好有同步器類AQS
     * AQS已經把大部分的方法都實現好了,我們只需要知道具體作用,調用即可
     */

    MySync sync = new MySync();
    //加鎖
    @Override
    public void lock() {
        //在外部直接調用同步器類的相關方法
        sync.acquire(1);
    }

    //加鎖,可打斷
    @Override
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    //嘗試加鎖
    @Override
    public boolean tryLock() {
       return sync.tryAcquire(1);
    }

    //嘗試加鎖,帶超時時間
    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1,unit.toNanos(time));
    }

    //釋放鎖
    @Override
    public void unlock() {
        //注意tryRelease()不會喚醒等待隊列中的線程,但是release()方法會喚醒等待隊列中的線程
         sync.release(0);
    }

    //創建條件變量
    @Override
    public Condition newCondition() {
        return sync.newCondition();
    }
}

測試結果:

21:29:03.742 c.TestAQS [t1] - locking...
21:29:04.755 c.TestAQS [t1] - unlocking...
21:29:04.755 c.TestAQS [t2] - locking...
21:29:04.756 c.TestAQS [t2] - unlocking...

3、ReentrantLock原理概述

如果有一個線程過來嘗試用ReentrantLock的lock()方法進行加鎖,會發生什麼事情呢?

AQS對象內部有一個核心的變量叫做state,是int類型的並且加了volatile關鍵字,代表了加鎖的狀態。初始狀態下,這個state的值是0。

AQS內部還有一個關鍵變量,用來記錄當前加鎖的是哪個線程,初始化狀態下,這個變量是null。

在這裏插入圖片描述

接着線程1跑過來調用ReentrantLock的lock()方法嘗試進行加鎖,這個加鎖的過程,直接就是用CAS操作將state值從0變爲1。如果之前沒人加過鎖,那麼state的值肯定是0,此時線程1就可以加鎖成功。一旦線程1加鎖成功了之後,就可以設置當前加鎖線程是自己。

在這裏插入圖片描述
說白了,AQS就是併發包裏的一個核心組件,裏面有state變量、加鎖線程變量等核心的東西,維護了加鎖狀態。你會發現,ReentrantLock這種東西只是一個外層的API,內核中的鎖機制實現都是依賴AQS組件的

可重入加鎖其實每次線程1可重入加鎖一次,會判斷一下當前加鎖線程就是自己,那麼他自己就可以可重入多次加鎖,每次加鎖就是把state的值給累加1,別的沒啥變化。

接着,如果線程1加鎖了之後,線程2跑過來加鎖會怎麼樣呢?

線程2跑過來一下看到,state的值不是0啊?所以CAS操作將state從0變爲1的過程會失敗,因爲state的值當前爲1,說明已經有人加鎖了!

接着線程2會看一下,是不是自己之前加的鎖啊?當然不是了,**“加鎖線程”**這個變量明確記錄了是線程1佔用了這個鎖,所以線程2此時就是加鎖失敗。
在這裏插入圖片描述

線程2會將自己放入AQS中的一個等待隊列,因爲自己嘗試加鎖失敗了,此時就要將自己放入隊列中來等待,等待線程1釋放鎖之後,自己就可以重新嘗試加鎖了

在這裏插入圖片描述

接着,線程1在執行完自己的業務邏輯代碼之後,就會釋放鎖!他釋放鎖的過程非常的簡單,就是將AQS內的state變量的值遞減1,如果state值爲0,則徹底釋放鎖,會將“加鎖線程”變量也設置爲null!
在這裏插入圖片描述

接下來,會從等待隊列的隊頭喚醒線程2重新嘗試加鎖。

好!線程2現在就重新嘗試加鎖,這時還是用CAS操作將state從0變爲1,此時就會成功,成功之後代表加鎖成功,就會將state設置爲1。

此外,還要把**“加鎖線程”**設置爲線程2自己,同時線程2自己就從等待隊列中出隊了。

在這裏插入圖片描述

AQS就是一個併發包的基礎組件,用來實現各種鎖,各種同步組件的。它包含了state變量、加鎖線程、等待隊列等併發中的核心組件。

ReentrantLock就是使用AQS而實現的一把鎖,它實現了可重入鎖,公平鎖和非公平鎖。它有一個內部類用作同步器是Sync,Sync是繼承了AQS的一個子類,並且公平鎖和非公平鎖是繼承了Sync的兩個子類。ReentrantLock的原理是:假設有一個線程A來嘗試獲取鎖,它會先CAS修改state的值,從0修改到1,如果修改成功,那就說明獲取鎖成功,設置加鎖線程爲當前線程。如果此時又有一個線程B來嘗試獲取鎖,那麼它也會CAS修改state的值,從0修改到1,因爲線程A已經修改了state的值,那麼線程B就會修改失敗,然後他會判斷一下加鎖線程是否爲自己本身線程,如果是自己本身線程的話它就會將state的值直接加1,這是爲了實現鎖的可重入。如果加鎖線程不是當前線程的話,那麼就會將它生成一個Node節點,加入到等待隊列的隊尾,直到什麼時候線程A釋放了鎖它會喚醒等待隊列隊頭的線程。這裏還要分爲公平鎖和非公平鎖,默認爲非公平鎖,公平鎖和非公平鎖無非就差了一步。如果是公平鎖,此時又有外來線程嘗試獲取鎖,它會首先判斷一下等待隊列是否有第一個節點,如果有第一個節點,就說明等待隊列不爲空,有等待獲取鎖的線程,那麼它就不會去同步隊列中搶佔cpu資源。如果是非公平鎖的話,它就不會判斷等待隊列是否有第一個節點,它會直接前往同步對列中去搶佔cpu資源。

4、ReentrantLock源碼分析

在這裏插入圖片描述

1、非公平鎖底層

lock()方法的邏輯: 多個線程調用lock()方法, 如果當前state爲0, 說明當前沒有線程佔有鎖, 那麼只有一個線程會CAS獲得鎖, 並設置此線程爲獨佔鎖線程。那麼其它線程會調用acquire方法來競爭鎖(後續會全部加入同步隊列中自旋或掛起)。當有其它線程A又進來想要獲取鎖時, 恰好此前的某一線程恰好釋放鎖, 那麼A會恰好在同步隊列中所有等待獲取鎖的線程之前搶先獲取鎖。也就是說所有已經在同步隊列中的尚未被 取消獲取鎖 的線程是絕對保證串行獲取鎖,而其它新來的卻可能搶先獲取鎖。

public class ReentrantLock implements Lock, java.io.Serializable {
    
    private final Sync sync;
	
    abstract static class Sync extends AbstractQueuedSynchronizer {
    	//......
    }
	
   	//非公平鎖繼承自Sync,Sync繼承自AQS,在AQS中定義了加鎖釋放鎖,同步狀態,同步隊列等
    static final class NonfairSync extends Sync {
       	//加鎖
        final void lock() {
            //利用CAS將status=0,改成status=1
            if (compareAndSetState(0, 1))
                //如果CAS成功,就會將Owner線程指向當前線程
                setExclusiveOwnerThread(Thread.currentThread());
            else
                //如果CAS失敗,代表出現競爭,就會進入這個方法
                acquire(1);
        }
		
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
}

在沒有競爭時:線程Thread-0通過CAS將同步狀態從0設置爲1,如果成功,就會將加鎖線程設置爲當前線程

在這裏插入圖片描述

當第一個競爭出現時:

在這裏插入圖片描述

1、acquire(1)

線程Thread-1同樣想通過CAS操作將status從0修改爲1,但是此時CAS失敗,就會進入acquire(1)方法:

//如果出現競爭,就會進入 acquire(1)方法
public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

2、tryAcquire(1)

由於Thread-1執行tryAcquire(1)方法一定會返回false(因爲CAS會失敗)

//嘗試獲取鎖,這個AQS中的方法,內部邏輯需要由子類去實現
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

tryAcquire方法仍然嘗試獲取鎖(快速獲取鎖機制),成功返回false,如果沒有成功, 那麼就將此線程包裝成Node加入同步隊列尾部。。Node.EXCLUSIVE 爲null表示這是獨佔鎖,如果爲讀寫鎖,那就是 共享模式(shared)。

3、addWaiter(Node mode)

!tryAcquire(arg)=true,接着就會執行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法:

//創建一個節點對象,並把它加入到等待隊列的隊尾
private Node addWaiter(Node mode) {
    //創建一個節點對象node
    Node node = new Node(Thread.currentThread(), mode);
    //指向tail節點的同步隊列的尾節點
    Node pred = tail;
    //如果尾節點不爲null,說明同步隊列已經初始化
    if (pred != null) {
        //將節點對象node的prev指向pred(同步隊列隊尾的節點)
        node.prev = pred;
        /**
        	這裏爲什麼需要CAS操作?(自我理解,多多指教)
        	如果線程Thread-0已經將同步狀態從status=0修改成了status=1
        	此時又有其他線程Thread-1,Thread-2,Thread-3,...,來進行CAS操作,試圖將status=0改成1
        	但是Thread-1,Thread-2,Thread-3,...,都會導致CAS操作失敗。
        	被阻塞的線程會被構造成一個節點對象通過CAS操作加入到同步隊列的尾部,之所以使用CAS操作,是因爲			 tail指向該節點Thread-3時,可能Thread-2已經更改了tail的值,主要是爲了保證線程安全和提高效率
        */
        
        /**
            因爲tail是加了volatile關鍵字的,因此其數據的更改會被同步到主存中,被其他線程可見,
            在setTail之前會比較獲取到的舊值pred和重新從主存中獲取的新值進行比較:
            如果一致,就將node更新爲pred,那麼此時新的節點node指向tial節點
            如果不一致,CAS失敗,就是說將線程加入到同步隊列失敗
        */
        //通過CAS操作設置新節點node爲尾節點,指向tail節點,快速添加尾節點
        if (compareAndSetTail(pred, node)) {
            //CAS成功後,node成爲尾節點,將pred節點的next指針指向node
            pred.next = node;
            return node;
        }
    }
    //1、如果尾節點爲空,說明隊列還未初始化,需要初始化head節點並將node加入到隊列尾部
    //2、如果將節點快速添加到隊尾中,沒添加進去,會繼續執行enq(node)方法以CAS自旋的方式添加,知道添加進去爲止
    enq(node);
    return node;
}

addWaiter(Node mode)方法的作用就是將當前線程構造成一個節點對象然後加入到同步隊列的隊尾,這個方法有兩層邏輯:

  1. 如果該線程是第一個出現競爭的線程Thread-1,那麼就說明隊列尾空,會直接執行enq(node)方法,先初始化同步隊列,構造一個哨兵節點,該節點不關聯任何線程,然後將當前線程節點加入到隊列的尾部,同步器通過“死循環”來保證節點正確被添加,在“死循環”中只有通過CAS將節點設置爲尾節點之後,當前線程才能從該方法中返回,否則,當前線程不斷通過CAS嘗試設置。
  2. 如果該線程不是第一個出現競爭的線程Thread-2,那麼說明同步隊列不爲null,首先會執行該方法快速將該節點添加到同步對隊列的隊尾,如果沒有添加成功,會繼續執行enq(node)方法,以CAS自旋將該節點添加到隊尾中。

總結addWaiter 邏輯:

  1. Node包裝當前線程
  2. pred 尾指針不爲null,即隊列不爲空, 則快速CAS將自己設爲新的tail
  3. 如果隊列爲空, 則調用enq強制入隊
  4. 如果CAS設置失敗,說明在其它線程入隊節點爭搶了tail,則此線程只能調用enq強制入隊

4、enq(final Node node)

  1. 由於Thread-1是第一個要競爭鎖的線程,因此同步隊列尾節點爲null,說明隊列還未初始化,需要初始化head節點,然後將當前節點添加到同步隊列的隊尾
  2. 將Thread-2節點添加到隊尾中沒有添加成功

以上兩種情況都會執行enq(final Node node)方法:

//初始化同步隊列,設置頭節點和尾節點
/**
同步器通過“死循環”來保證節點正確被添加,在“死循環”中只有通過CAS將節點設置爲尾節點之後,當前線程才能從該方法中返回,否則,當前線程不斷通過CAS嘗試設置。
*/
private Node enq(final Node node) {
	//死循環,CAS自旋,多次嘗試,直到成功爲止
    for (;;) {
        //指向tail節點的同步隊列的尾節點
        Node t = tail;
        //如果t=null,說明隊列還沒有初始化,需要先通過CAS自旋設置一個頭結點head
        if (t == null) { 
            if (compareAndSetHead(new Node()))
                //該head 稱爲 Dummy(啞元)或哨兵,用來佔位,並不關聯線程
                tail = head;
        //如果t!=null,說明隊列已經初始化,那麼會讓節點t以自旋的方式加入到隊列的尾部
        } else {
            //將當前節點的prev指向t
            node.prev = t;
            //通過CAS的方式將t節點更新爲node節點,就是將node節點加入到隊列的尾部,此時node節點指向tail
            if (compareAndSetTail(t, node)) {
                //如果CAS成功,說明node節點成功添加到同步隊列尾部
                t.next = node;
                return t;
            }
        }
    }
}

方法內是一個for(;;),看來退出的條件只能是當前線程入隊成功。之前也提到過,只有在產生鎖競爭了,纔會去初始化鏈表頭節點。如果隊列爲空,初始化頭尾節點,然後後續循環會走到else,else的邏輯和上線CAS入隊的邏輯一樣,只不過這裏套在for循環裏,直到入隊成功才退出循環。

addWaiter的實現比較簡單且實現功能明瞭:把當前線程構造成一個節點node對象,加入到同步隊列隊尾。

在這裏插入圖片描述

5、acquireQueued()

這個方法讓已經入隊的線程嘗試獲取鎖,若失敗則會被掛起。

final boolean acquireQueued(final Node node, int arg) {
    //標記是否成功獲取鎖
    boolean failed = true;
    try {
        //標記是否被打斷
        boolean interrupted = false;
        //死循環,調用tryAcquire(arg)方法,CAS自旋不斷嘗試獲取鎖
        for (;;) {
            //獲取node節點的前驅節點
            final Node p = node.predecessor();
            //如果前驅節點爲頭節點Head,即該結點已成老二,那麼便有資格去嘗試獲取鎖
            //可能是老大釋放完資源喚醒自己的,當然也可能被interrupt了
            //執行tryAcquire(arg)方法嘗試獲取鎖
            if (p == head && tryAcquire(arg)) {
                //如果該節點獲取到了鎖,就將當前節點設置爲頭節點
                setHead(node);
                //原head節點出隊,在某個時間點被GC回收
                p.next = null; // help GC
                //獲取鎖成功
                failed = false;
                //返回等待的過程中是否被中斷過
                return interrupted;
            }
            
            //如果上一步獲取鎖失敗後,判斷是否可通過park()進入waiting狀態,直到被unpark()
            //如果不可中斷的情況下被中斷了,會從park()中醒過來,發現拿不到資源,從而繼續進入park()等待。
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                //如果等待過程中被中斷過,哪怕只有那麼一次,就將interrupted標記爲true
                interrupted = true;
        }
    } finally {
        if (failed)
            //若等待過程中沒有成功獲取資源(timeout,或可中斷的情況下被中斷了),取消結點在隊列中的等待。
            cancelAcquire(node);
    }
}

failed 標記最終acquire是否成功, interrupted標記是否曾被掛起過。注意到for(;;) 跳出的唯一條件就是if (p == head && tryAcquire(arg)) 即當前線程結點是頭結點且獲取鎖成功。從這裏我們應該看到,這是一個線程第三次又想着嘗試快速獲取鎖:雖然此時該節點已被加入等待隊列,在進行睡眠之前又通過p == head && tryAcquire(arg)方法看看能否獲取鎖。也就是說只有該線程結點的所有 有效的前置結點都拿到過鎖了,當前結點纔有機會爭奪鎖,如果失敗了那就通過shouldParkAfterFailedAcquire方法判斷是否應該掛起當前結點,等待響應中斷。

6、shouldParkAfterFailedAcquire()

/**
	判斷當前線程獲取鎖失敗之後是否需要掛起.
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    //獲取前驅節點的等待狀態waitStatus
    int ws = pred.waitStatus;
    //如果前驅節點的等待狀態爲Node.SIGNAL=-1,就說明當前節點可以休息了
    //線程入隊後能夠掛起的前提是,它的前驅節點的狀態爲SIGNAL,它的含義是“Hi,前面的兄弟,如果你獲取鎖並且出隊後,記得把我喚醒!”。所以shouldParkAfterFailedAcquire會先判斷當前節點的前驅是否狀態符合要求,若符合則返回true。然後調用parkAndCheckInterrupt,將自己掛起。
    if (ws == Node.SIGNAL)
        return true;
    
    //如果ws > 0,前驅節點的狀態爲CANCELLED=1
    //如果ws>0說明前置結點是被自己取消獲取同步的結點(只有線程本身可以取消自己)。
	//那麼do while循環一直往頭結點方向找waitStatus < 0的節點;
	//含義就是去除了FIFO隊列中的已經自我取消申請同步狀態的線程。
    if (ws > 0) {
        do {
            //從隊尾向前尋找第一個狀態不爲CANCELLED=1的節點
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        //如果ws<=0,那麼就通過CAS將pred的前驅節點的等待狀態改成Node.SIGNAL=-1
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

7、parkAndCheckInterrupt()

// 掛起當前線程,返回線程中斷狀態並重置
private final boolean parkAndCheckInterrupt() {
    //調用park()方法阻塞當前線程
    LockSupport.park(this);
    //返回中斷狀態,如果沒有線程打斷該線程的休息,就會返回false,一旦有其他線程打斷睡眠,就會返回true
    return Thread.interrupted();
}

具體總結下上面這三個方法的作用:處於等待隊列中的線程Thread-1嘗試獲取鎖的過程

在這裏插入圖片描述

執行acquireQueued()方法內的第一次for循環,第一次嘗試獲取鎖:

獲取Thread-1線程節點的前驅節點,並判斷該節點是不是頭結點Head,如果是的話,Thread-1線程就會繼續執行tryAcquire(1)方法嘗試獲取鎖,很明顯CAS會失敗,因爲Thread-0已經獲取了同步資源。失敗後接着會向下執行shouldParkAfterFailedAcquire()方法,該方法首先會獲取前驅節點的等待狀態waitStatus:

如果waitStatus=-1,就說明當Thread-1節點的前驅節點獲取鎖並且出隊後會把Thread-1喚醒(“Hi,前面的兄弟,如果你獲取鎖並且出隊後,記得把我喚醒!”),同時return true;接着會繼續執行parkAndCheckInterrupt()方法讓Thread-1線程掛起(調用park()方法讓該線程阻塞住,不用一直CAS自旋嘗試獲取鎖)。

很明顯Thread-1線程的前驅節點的waitStatus=0,那麼就會執行compareAndSetWaitStatus(pred, ws, Node.SIGNAL);方法將waitStatus設置爲-1,同時return false

在這裏插入圖片描述

執行acquireQueued()方法內的第一次for循環,第二次嘗試獲取鎖:

首先獲取Thread-1線程節點的前驅節點,並判斷該節點是不是head,如果是的話,Thread-1線程戶繼續執行tryAcquire(1)方法嘗試獲取鎖,由於Status=1,因此獲取CAS失敗,就是說該線程有一次通過CAS獲取鎖失敗。失敗後會接着繼續向下執行shouldParkAfterFailedAcquire()方法,該方法首先會獲取前驅節點的等待狀態waitStatus,進行判斷:

如果waitStatus=-1,就說明Thread-1節點的前驅節點獲取鎖並出隊後會把Thread-1線程喚醒,同時return true。接着會繼續執行parkAndCheckInterrupt()方法讓Thread-1線程掛起(調用park()方法讓該線程阻塞住,不用一直CAS自旋嘗試獲取鎖)。在掛起的過程中,如果沒有被其他線程打斷,就會一直處於阻塞狀態。如果在阻塞的過程中被其他線程打斷了,那麼return Thread.interrupted();就會返回true。繼而acquireQueued()方法就會繼續向下執行interrupted = true;,同時被打斷的線程會繼續執行for循環嘗試獲取鎖,如果獲取不到會繼續調用park()進入阻塞狀態。

再次有多個線程經歷上述過程競爭失敗,變成這個樣子:
在這裏插入圖片描述

8、release(int arg)

Thread-0 釋放鎖,進入 tryRelease 流程

public void unlock() {
    sync.release(1);
}
  
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        //找到頭節點
        Node h = head;
        //當前隊列不爲 null,並且 head 的 waitStatus = -1,進入 unparkSuccessor 流程
        if (h != null && h.waitStatus != 0)
            //喚醒等待隊列裏的下一個線程
            unparkSuccessor(h);
        return true;
    }
    return false;
}

9、tryRelease(int arg)

protected boolean tryRelease(int arg) {
	throw new UnsupportedOperationException();
}

跟tryAcquire()一樣,這個方法是需要獨佔模式的自定義同步器去實現的。Thread-0 釋放鎖,進入 tryRelease 流程,如果成功設置 exclusiveOwnerThread 爲 null、state = 0
在這裏插入圖片描述

10、unparkSuccessor()

//找到隊列中離 head 最近的一個 Node(沒取消的),unpark 恢復其運行,本例中即爲 Thread-1
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    
	//找到下一個需要喚醒的結點s
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 從後向前找
        for (Node t = tail; t != null && t != node; t = t.prev) 
            if (t.waitStatus <= 0)  s = t;
    }
    if (s != null)
        //喚醒當前線程
        LockSupport.unpark(s.thread);
}

release()是獨佔模式下線程釋放共享資源的頂層入口。它會釋放指定量的資源,如果徹底釋放了(即state=0),它會喚醒等待隊列裏的其他線程來獲取資源。

在release(1)中,如果當前隊列不爲 null,並且 head 的 waitStatus = -1,進入 unparkSuccessor 流程,找到隊列中離 head 最近的一個 Node(沒取消的),unpark 恢復其運行,本例中即爲 Thread-1,回到 Thread-1 的 acquireQueued 流程 ,s被喚醒後,進入if (p == head && tryAcquire(arg))的判斷(即使p!=head也沒關係,它會再進入shouldParkAfterFailedAcquire()尋找一個安全點。這裏既然s已經是等待隊列中最前邊的那個未放棄線程了,那麼通過shouldParkAfterFailedAcquire()的調整,s也必然會跑到head的next結點,下一次自旋p==head就成立啦),然後s把自己設置成head標杆結點,表示自己已經獲取到資源了,acquire()也返回了。

在這裏插入圖片描述

如果加鎖成功(沒有競爭),會設置exclusiveOwnerThread 爲 Thread-1,state = 1。head 指向剛剛 Thread-1 所在的 Node,該 Node 清空 Thread。原本的 head 因爲從鏈表斷開,而可被垃圾回收 。

如果這時候有其它線程來競爭(非公平鎖的體現),例如這時有 Thread-4 來了
在這裏插入圖片描述

如果不巧又被 Thread-4 佔了先,Thread-4 被設置爲 exclusiveOwnerThread,state = 1。Thread-1 再次進入 acquireQueued 流程,獲取鎖失敗,重新進入 park 阻塞 。

總結:根據源碼畫了一個流程圖,奈何頁面太小,字體顯示的不清楚
在這裏插入圖片描述

2、可重入鎖底層

final boolean nonfairTryAcquire(int acquires) {
    //獲取當前線程
    final Thread current = Thread.currentThread();
    //獲取state變量的值
    int c = getState();
    //如果state=0,說明還沒有線程來佔用鎖
    if (c == 0) {
        //通過CAS將state=0設置爲1
        if (compareAndSetState(0, acquires)) {
            //佔用鎖成功,將獨佔線程設置爲當前線程
            setExclusiveOwnerThread(current);
            //同時返回true,代表獲取鎖資源成功
            return true;
        }
    }
    //如果state=1,說明已經有其他線程獲取了鎖
    //判斷ExclusiveOwnerThread指向的線程是否是當前線程,就是說判斷是不是自己佔用了鎖,鎖重入原理
    else if (current == getExclusiveOwnerThread()) {
        //如果是自己佔有了鎖,就說明出現了鎖重入,那麼就讓state+=1
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        //更新state的重入次數
        setState(nextc);
        //返回true,同樣表示佔用鎖成功
        return true;
    }
    //如果是其他線程佔用了鎖,那麼獲取鎖失敗,返回false.
    return false;
}

原理:檢查state字段,若爲0,表示鎖未被佔用,那麼嘗試佔用,若不爲0,檢查當前鎖是否被自己佔用,若被自己佔用,則更新state字段,表示重入鎖的次數。如果以上兩點都沒有成功,則獲取鎖失敗,返回false。

3、公平鎖和非公平鎖的區別

兩者的區別主要體現在加鎖過程上的區別,即tryAcquire(1)方法的不同:

對於非公平鎖的tryAcquire(1)方法:

final boolean nonfairTryAcquire(int acquires) {
    //獲取當前線程
    final Thread current = Thread.currentThread();
    //獲取state變量的值
    int c = getState();
    //如果state=0,說明還沒有線程來佔用鎖
    if (c == 0) {
        //通過CAS將state=0設置爲1
        if (compareAndSetState(0, acquires)) {
            //佔用鎖成功,將獨佔線程設置爲當前線程
            setExclusiveOwnerThread(current);
            //同時返回true,代表獲取鎖資源成功
            return true;
        }
    }
    //如果state=1,說明已經有其他線程獲取了鎖
    //判斷ExclusiveOwnerThread指向的線程是否是當前線程,就是說判斷是不是自己佔用了鎖
    else if (current == getExclusiveOwnerThread()) {
        //如果是自己佔有了鎖,就說明出現了鎖重入,那麼就讓state+=1
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        //更新state的重入次數
        setState(nextc);
        //返回true,同樣表示佔用鎖成功
        return true;
    }
    //如果是其他線程佔用了鎖,那麼獲取鎖失敗,返回false.
    return false;
}
  1. 檢查state字段,若爲0,表示鎖未被佔用,那麼嘗試佔用
  2. 若不爲0,檢查當前鎖是否被自己佔用,若被自己佔用,則更新state字段,表示重入鎖的次數。
  3. 如果以上兩點都沒有成功,則獲取鎖失敗,返回false。

非公平鎖,首先是檢查並設置鎖的狀態,如果state=0,就會去嘗試加鎖,並不會到同步隊列中等待獲取鎖,這種方式會出現即使隊列中有等待的線程,但是新的線程仍然會與同步隊列中等待獲取同步狀態的線程競爭,所以新的線程可能會搶佔已經在排隊的線程的鎖,這樣就無法保證先來先服務,但是已經等待的線程們是仍然保證先來先服務的。

對於非公平鎖的tryAcquire(1)方法:

static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
        acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        //獲取當前線程
        final Thread current = Thread.currentThread();
        //獲取state變量的值
        int c = getState();
        //如果state=0
        if (c == 0) {
            //判斷AQS隊列中是否有前驅節點,如果沒有就嘗試獲取鎖資源
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                //將當前線程設置爲加鎖線程
                setExclusiveOwnerThread(current);
                //結果返回true,表示加鎖成功
                return true;
            }
        }
        //判加鎖線程是不是當前線程,可重入鎖的原理
        else if (current == getExclusiveOwnerThread()) {
            //如果是,就將同步狀態state+=1
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            //更新state字段表示鎖重入的次數
            setState(nextc);
            //結果返回true,表示加鎖成功
            return true;
        }
        //否則返回false
        return false;
    }
}

hasQueuedPredecessors() 方法:

和非公平鎖對比多了這個方法邏輯, 也就意味着沒有了新來線程插隊的情況,保證了公平鎖的獲取串行化。

public final boolean hasQueuedPredecessors() {
    Node t = tail; 
    Node h = head;
    Node s;
    //h != t表示隊列中有node,
    return h != t &&
        //表示隊列中還沒有老二,或者隊列中的老二線程不是當前線程
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

在公平鎖中,每一次的tryAcquire都會檢查CLH隊列中是否仍有等待獲取鎖資源的線程,如果有返回false,進而自己也加入到等待隊列中,通過這種方式來保證先來先服務的原則。

4、公平鎖和非公平鎖的釋放

public void unlock() {
   sync.release(1);
}

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        //找到頭節點
    	Node h = head;
    if (h != null && h.waitStatus != 0)
        //喚醒下一個線程
        unparkSuccessor(h);
        return true;
    }
	return false;
}

//這個方法由子類去實現,這裏公平鎖和非公平鎖的實現類邏輯相同
protected boolean tryRelease(int arg) {
   throw new UnsupportedOperationException();
}

自定義同步器ReentrantLock方法中 tryRelease()方法的具體實現類:

abstract static class Sync extends AbstractQueuedSynchronizer {
    
    //嘗試釋放鎖
    protected final boolean tryRelease(int releases) {
        //計算鎖重入的次數
        int c = getState() - releases;
        //如果佔有鎖的線程不是當前線程,就拋出異常
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        //如果state=0,說明鎖重入次數爲0,表示釋放成功
        if (c == 0) {
            free = true;
            //將owner設置爲null
            setExclusiveOwnerThread(null);
        }
        //重置鎖重入的次數
        setState(c);
        return free;
    }
}

5、不可中斷原理

在此模式下,即使它被打斷,仍會駐留在 AQS 隊列中,一直要等到獲得鎖後方能得知自己被打斷了

/**
    1、假如Thread-0線程正在阻塞狀態,其他線程如Thread-1找到THread-0線程對象,
       調用interrupt()方法將其打斷,那麼此時該方法的結果就會返回true
*/
private final boolean parkAndCheckInterrupt() {
	//讓該線程阻塞,如果打斷標記爲true,那麼park()方法失效
    LockSupport.park(this);
    //返回是否被打斷過,如果被打斷了返回true,否則返回false,同時還會清除打斷標記
    return Thread.interrupted();
}

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        /**
        	3、當線程被打斷後,就會醒過來,醒過來以後將interrupted=true
        	  然後重新去判斷if (p == head && tryAcquire(arg)) 並嘗試獲取鎖
        	  只有獲取鎖成功,纔會返回這個打斷標記interrupted=true
        */
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            /**
            	2、當parkAndCheckInterrupt()返回true後,就會繼續向下執行,
            		將打斷標記置爲true,但是並用到這個打斷標記,進而繼續進行下一次的for循環
            */
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                //將打斷標記置爲true,同時繼續進行下一次的for循環
                interrupted = true;
            }
        } finally {
            if (failed)
            cancelAcquire(node);
    }
}
    
public final void acquire(int arg) {
    /**
    	4、如果acquireQueued()返回true,就會繼續執行selfInterrupt()方法
    */
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

static void selfInterrupt() {
    //重新產生一次中斷,即改變了中斷狀態,interrupted=true
	Thread.currentThread().interrupt();
}

總結:

  1. 假如Thread-0線程正在阻塞狀態在同步隊列中等待獲取鎖,其他線程如Thread-1找到Thread-0線程對象, 調用interrupt()方法將其打斷,那麼此時該方法的結果就會返回true
  2. 當parkAndCheckInterrupt()返回true後,就會繼續向下執行,打斷標記置爲true,但是並用到這個打斷標記,進而繼續進行下一次的for循環(繼續向下運行了,就是說被打斷後並沒有放棄等待獲取鎖資源)
  3. 當線程被打斷後,就會醒過來,醒過來以後將interrupted=true,執行for循環, 然後重新去判斷if (p == head && tryAcquire(arg)) 並嘗試獲取鎖,只有獲取鎖成功,纔會返回這個打斷標記interrupted=true。
  4. 如果acquireQueued()返回true,就會繼續執行selfInterrupt()方法,停止等待鎖資源

總的來說,如果其他線程打斷了當前線程的阻塞狀態,當前線程醒來後也不會放棄獲取鎖,而是繼續向下執行,嘗試獲取鎖,獲取不成功,繼續進入阻塞狀態。即當前線程會一直在同步隊列中死等,直到獲取了鎖爲止。

6、可中斷原理

static final class FairSync extends Sync {
	//...
    //調用ReentrantLock的lockInterruptibly()方法,就會執行AQS內的acquireInterruptibly
    //其實ReentrantLock就相當於AQS的外層API
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
}

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    public final void acquireInterruptibly(int arg) throws InterruptedException {
        //判斷線程是否處於中斷狀態,如果處於就說明中斷狀態爲true,同時清除打斷標記,將中斷狀態設置爲false
        if (Thread.interrupted())
            //如果線程處於打斷狀態,就會直接拋出異常,由於這個異常並沒有別捕獲,因此後面的代碼就不會再執行
            throw new InterruptedException();
        //嘗試獲取鎖,如果CAS失敗就會繼續向下執行(2)
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }
    
    //(2)
    
    /**
    	2、如果parkAndCheckInterrupt()返回true,繼續執行throw new InterruptedException();
    	拋出異常就不會再進入死循環for(;;)中,這也是終止線程的方法之一
    	拋出異常後,那麼線程就會停止等待獲取鎖,即不會再死等了
    */
    private void doAcquireInterruptibly(int arg) throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
           
                if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    
    /**
    	1、假如Thread-0線程正在阻塞狀態,其他線程如Thread-1找到THread-0線程對象,
           調用interrupt()方法將其打斷,那麼此時該方法的結果就會返回true
    */
    private final boolean parkAndCheckInterrupt() {
        //讓該線程阻塞,如果打斷標記爲true,那麼park()方法失效
        LockSupport.park(this);
        //返回是否被打斷過,如果被打斷了返回true,否則返回false,同時還會清除打斷標記
        return Thread.interrupted();
    }
}

總結:

  1. 假如Thread-0線程正在阻塞狀態在同步隊列中等待獲取鎖,其他線程如Thread-1找到Thread-0線程對象, 調用interrupt()方法將其打斷,那麼此時該方法的結果就會返回true
  2. 當parkAndCheckInterrupt()返回true後,就會繼續向下執行,拋出異常,不再執行for()循環,方法執行結束,線程終止,停止等待獲取鎖資源。

7、條件變量await()原理

每個條件變量其實就對應着一個條件隊列,其實現類是ConditionObject,該類是AQS的內部類,一個ConditionObject是一條等待隊列。

1、await()方法

public class ConditionObject implements Condition, java.io.Serializable {
    
    /** 等待隊列的頭節點 */
    private transient Node firstWaiter;
    /** 等待隊列的尾節點 */
    private transient Node lastWaiter;
    
    public final void await() throws InterruptedException {
        //如果等待的線程別打斷,那麼直接拋出異常InterruptedExceptio
        if (Thread.interrupted())
            throw new InterruptedException();
        //當前線程封裝成一個Node節點,並將該節點添加到ConditionObject的等待隊列中
        Node node = addConditionWaiter();
        //接下來進入 AQS 的 fullyRelease 流程,釋放同步器上的鎖,返回節點狀態
        int savedState = fullyRelease(node);
        int interruptMode = 0;
        //判斷當前節點在不在同步隊列中,如果不在,說明是條件隊列中的節點
        while (!isOnSyncQueue(node)) {
            //調用park()將當前線程阻塞
            LockSupport.park(this);
            if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                break;
        }
        //當前線程被喚醒後會再次嘗試獲取鎖
        if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
            interruptMode = REINTERRUPT;
        if (node.nextWaiter != null) // clean up if cancelled
            unlinkCancelledWaiters();
        if (interruptMode != 0)
            reportInterruptAfterWait(interruptMode);
    }
}

2、addConditionWaiter()方法

調用addConditionWaiter()方法將當前線程封裝成一個Node節點,並將該節點添加到ConditionObject的等待隊列中

private Node addConditionWaiter() {
    Node t = lastWaiter;
    // If lastWaiter is cancelled, clean out.
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

開始 Thread-0 持有鎖,調用 await,進入 ConditionObject 的 addConditionWaiter 流程,創建新的 Node 狀態爲 -2(Node.CONDITION),關聯 Thread-0,加入條件隊列尾部

在這裏插入圖片描述

3、fullyRelease(node)方法

接下來進入 AQS 的 fullyRelease 流程,釋放同步器上的鎖

釋放鎖,返回同步狀態。釋放鎖失敗的話,節點的waitStatus爲CANCELLED。

final int fullyRelease(Node node) {
    boolean failed = true;
    try {
        int savedState = getState();
        if (release(savedState)) {
            failed = false;
            return savedState;
        } else {
            throw new IllegalMonitorStateException();
        }
    } finally {
        if (failed)
            node.waitStatus = Node.CANCELLED;
    }
}

在這裏插入圖片描述

unpark AQS 隊列中的下一個節點,競爭鎖,假設沒有其他競爭線程,那麼 Thread-1 競爭成功

在這裏插入圖片描述

總結:

  1. 調用addConditionWaiter()方法將當前線程封裝成一個Node節點,並將該節點添加到ConditionObject的等待隊列中
  2. 調用fullyRelease(node)方法將當前線程所持有的鎖資源完全釋放
  3. while (!isOnSyncQueue(node)){…},判斷當前線程的節點是否在AQS的同步隊列中,因爲當前線程的節點剛剛被加入到ConditionObject的等待隊列中,所以isOnSyncQueue(node)方法返回false,取反則會進入到while循環體中
  4. 調用LockSupport.park(this)方法,將當前線程阻塞,此時線程的狀態爲WAITING。此時線程等待被喚醒,喚醒後的線程會從LockSupport.park(this)方法開始繼續向下執行。
  5. 當線程被喚醒後,執行語句if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)判斷當前線程的喚醒方式,以及對應的後續操作。

有兩種方法可以將一個狀態爲WAITING的線程喚醒

  • 調用LockSupport.unpark(Thread thread)方法。
  • 調用thread.interrupt方法。沒錯,調用中斷方法會將一個線程喚醒

8、條件變量的single()原理

假設 Thread-1 要來喚醒 Thread-0,進入 ConditionObject 的 doSignal 流程,取得等待隊列中第一個 Node,即 Thread-0 所在 Node 。

1、single()方法

public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

2、doSignal(first)方法

將Condition的等待隊列的頭結點出隊,並將符合條件的節點添加到AQS的等待隊列中

進入 ConditionObject 的 doSignal 流程,取得等待隊列中第一個 Node,即 Thread-0 所在 Node

private void doSignal(Node first) {
    do {
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

3、transferForSignal(Node node)方法

執行 transferForSignal 流程,將該 Node 加入 AQS 隊列尾部,將 Thread-0 的 waitStatus 改爲 0,Thread-3 的
waitStatus 改爲 -1

final boolean transferForSignal(Node node) {
    //如果node節點的等待狀態不爲Node.CONDITION,則表示該節點的線程已經被取消或者在調用signal()
    //方法之前已經被其他線程中斷,所以直接將該節點從condition的等待隊列中剔除
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    //將當前節點添加到AQS的同步隊列中,返回值p爲當前節點在阻塞隊列的前驅節點
    Node p = enq(node);
    int ws = p.waitStatus;
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}
  1. ws > 0 的情況是當前節點在AQS阻塞隊列的前驅節點的狀態爲Node.CANCELLED,此時將會喚醒當前節點,並在acquireQueued方法中調用shouldParkAfterFailedAcquire方法將狀態爲Node.CANCELLED的節點從AQS的阻塞隊列中剔除
  2. !compareAndSetWaitStatus(p, ws, Node.SIGNAL) 爲true,即當前線程準備通過CAS將前驅節點的狀態改爲Node.SIGNAL失敗。從AQS獲取鎖和釋放鎖的代碼中可以看出,AQS阻塞隊列節點的前驅節點狀態必須要爲Node.SIGNAL。CAS修改失敗可能可能的場景是在執行CAS的時候,前驅節點線程的狀態變爲Node.CANCELLED。

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章