天天看點

AQS及ReentrantLock的底層原理AQS(Abstract Queued Synchronzied)

寫在前面,本文受啟與養兔子的大叔的文章,并在他文章的基礎上添加了一些自己的了解。

AQS(Abstract Queued Synchronzied)

AQS的底層其實就是 “作業系統基礎” 中的狀态量和鎖的V-P操作。

AQS就是JDK中為“線程同步”提供的一套基礎工具類,其實就是一個類而已,其上文講到的同步元件(CountDownLatch,CyclicBarrier,Semaphore)(是以AQS就成了非常重要的一個知識點,因為基于它可以寫出JAVA中的很多“鎖”類。比如此文要分析的ReentrantLock,它就是基于AQS而形成了一個“可重入鎖”。

ReentrantLock

ReetrantLock中的lock方法,底層是調用了sync對象的方法,其中sync繼承了AQS,ReentrantLock中的lock方法調用了sync對象的lock方法,lock方法再調用acquire方法擷取同步狀态,acAquire方法中調用tryAcquire再次擷取同步狀态,如果失敗,則将線程加入以Node為節點形成連結清單實作的隊列中排隊。公平鎖跟非公平鎖的差別在于,擷取同步狀态時,如果狀态為0(可占領鎖的情況下),非公平鎖會讓目前線程嘗試占領該鎖,而公平鎖則需要判斷等待隊列中是否有線程在排隊,沒有排隊的情況下才能讓該線程占領此鎖。

了解ReentrantLock的主要方法

可以看出ReentrantLock對象實作了Lock接口。【這裡借用一下養兔子的大叔的圖及他的思想】

AQS及ReentrantLock的底層原理AQS(Abstract Queued Synchronzied)

而Lock接口的主要方法有以下幾個:

AQS及ReentrantLock的底層原理AQS(Abstract Queued Synchronzied)

Lock對象隻是一個接口,上述方法具體的實作其實都在ReentrantLock中,是以我們隻要在ReentrantLock對象中檢視具體實作去了解鎖的“加鎖”和“解鎖”操作是如何做的。

ReentrantLock的主要方法

AQS及ReentrantLock的底層原理AQS(Abstract Queued Synchronzied)

​ 注意:這裡有個Sync類型的sync屬性,此屬性很重要,下文将會講到

其中加鎖方法即為lock(),解鎖方法即為unLock()

這兩個方法在ReentrantLock 源碼中的實作如下

//lock
public void lock() {
    sync.lock();
}

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

           

從上述可以知道這兩個方法實際上是操作了一個叫做sync的對象,調用該對象的lock和release操作來實作。

sync是什麼東西?

上圖中我們可以看出sync是ReentrantLock中的一個私有的成員變量,且類型是Sync對象。

Sync是什麼類?做啥的?是什麼時候初始化的?

我們先看簡單的“sync是在什麼時候初始化的”

在源碼中,隻有在2個構造函數的地方對sync對象做了初始化,可分别初始化為NonfairSync(非公平)和FairSync(公平)

/** 所有鎖操作都是基于這個字段 */
private final Sync sync;
/**
 * 通過該構造函數建立額ReentrantLock是一個非公平鎖
 */
public ReentrantLock() {
    sync = new NonfairSync();
}
/**
 * 如果入參為true,則建立公平的ReentrantLock;
 * 否則,建立非公平鎖
 */
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
           

這兩個對象(NonfairSync和NonfairSync)也是ReentrantLock的内部類。

繼承關系:

AQS及ReentrantLock的底層原理AQS(Abstract Queued Synchronzied)

從上圖可以看出FairSync和NonFairSync在類結構上完全一樣且均繼承于Sync

而Sync對象繼承了AQS 其繼承關系如下

AQS及ReentrantLock的底層原理AQS(Abstract Queued Synchronzied)

​ ReentrantLock的内部類Sync實作于AQS類

以上我們可以得出這麼一個初步結論

ReentrantLock實作了Lock接口,操作其成員變量sync這個AQS的子類,來完成鎖的相關功能。而sync這個成員變量有2種形态:NonfairSync和FairSync

ReentrantLock的構造函數中,預設的無參構造函數将會把Sync對象建立為NonfairSync對象,這是一個“非公平鎖”;而另一個構造函數ReentrantLock(boolean fair)傳入參數為true時将會把Sync對象建立為“公平鎖”FairSync。

FairSync、NonfairSync、Sync之間的關系

在上文中我們提到ReentrantLock的lock操作是調用sync的lock方法。而sync有2種形态,那麼我們可以分别對比一下NonfairSync的lock方法和FairSync的lock方法有什麼異同。

  1. 首先是NonfairSync源碼中的lock方法:
final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}
           

lock方法使用CAS來嘗試将同步狀态改為1,如果成功則将同步狀态持有線程置為目前線程。否則說明擷取鎖失敗,将調用AQS提供的 acquire()方法。

我們順藤摸瓜,再來看AQS的 acquire()方法:

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
           

tryAcquire(arg)

:再次嘗試擷取同步狀态,成功直接方法退出,失敗調用addWaiter();

AQS及ReentrantLock的底層原理AQS(Abstract Queued Synchronzied)

addWaiter(Node.EXCLUSIVE), arg)

:将目前線程以指定模式(獨占式、共享式)封裝為Node節點後置入同步隊列。

2.再來看FairSync源碼中的lock方法。

//沒有嘗試占有該鎖,還是直接調用了AQS的acquire方法。
final void lock() {
    acquire(1);
}

//
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
           

tryAcquire方法了解如下:

AQS及ReentrantLock的底層原理AQS(Abstract Queued Synchronzied)

通過對比其源碼,我們可以看出其大同小異,底層都是調用了AQS的acquire方法。

FairSync在tryAquire方法中,當判斷到鎖狀态字段state == 0 時,不會立馬将目前線程設定為該鎖的占用線程,而是去判斷是在此線程之前是否有其他線程在等待這個鎖(執行hasQueuedPredecessors()方法),如果是的話,則該線程會加入到等待隊列中,進行排隊(FIFO,先進先出的排隊形式)。這也就是為什麼FairSync可以讓線程之間公平獲得該鎖。

NoFairSync的tryAquire方法中,沒有判斷是否有在此之前的排隊線程,而是直接進行獲鎖操作,是以多個線程之間同時争用一把鎖的時候,誰先擷取到就變得随機了,很有可能線程A比線程B更早等待這把鎖,但是B卻擷取到了鎖,A繼續等待(這種現象叫做:線程饑餓)

線程是什麼時候排隊的?

AQS的源碼 acquire方法

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
           

這個**addWaiter(Node.EXCLUSIVE)**就是請求排隊的,該動作是在FairSync或者NoFairSync調用AQS的aquire(1)方法時觸發的,而且由該方法中的if塊可知,隻有當if中的tryAcquire為false的時候(也就是獲鎖失敗的時候),才會執行後續的acquireQueued方法。

線程是如何排隊的?

想要知道如何排隊,那也就是去了解**addWaiter(Node.EXCLUSIVE)**這個方法具體是如何實作的。

隊列是連結清單的形式組織不同Node(每一個Node代表一個線程)之間的先後順序。

在AQS中目前線程、線程狀态、此線程的前後節點 被組織到了一個叫做Node的資料結構(内部類),再次不過多贅述,排隊的線程先後組成連結清單,隊列是連結清單的形式,組織不同Node(每一個Node代表一個線程)之間的先後順序。公平鎖依靠這個順序實作公平競争。

等待中的線程如何感覺到鎖空閑并獲得鎖?

簡單地講:就是一個線程擷取鎖失敗了,被放到了線程等待隊列中,而acquireQueued方法就是把放入隊列中的這個線程不斷進行“獲鎖”,直到它**“成功獲鎖”或者“不再需要鎖(如被中斷)” ,是一個自旋**的過程,且為了防止不停的自旋,其自旋過程中添加了一個判斷,判斷目前線程是否需要被阻塞,這樣就防止了自旋空跑。

關于公平與非公平鎖本質上的差別

“不管公平還是非公平模式下,ReentrantLock對于排隊中的線程都能保證,排在前面的一定比排在後面的線程優先獲得鎖”但是,這裡有個但是,非公平模式不保證“隊列中的第一個線程一定就比新來的(未加入到隊列)的線程優先獲鎖”因為隊列中的第一個線程嘗試獲得鎖時,可能剛好來了一個線程也要擷取鎖,而這個剛來的線程都還未加入到等待隊列,此時兩個線程同時随機競争,很有可能,隊列中的第一個線程競争失敗(而該線程等待的時間其實比這個剛來的線程等待時間要久)。

解鎖

在上文中我們可以得出:

隻要有其他線程因為釋放了鎖,那麼“線程等待隊列中的第一個Node節點就可以成功擷取到鎖(如果沒有隊列外的線程同時競争這個鎖)”

實際上這并不是一個很準确的結論,因為“線程等待隊列”中的第一個Node節點在其他線程未釋放鎖時,因為擷取不到鎖,那麼也會被“阻塞”

這個時候,實際上所有在等待隊列中的Node節點裡代表的線程都是處于“阻塞”狀态。

那什麼時候喚醒這些阻塞的線程呢?

既然申請鎖的時候會導緻線程在得不到鎖時被“阻塞”,那麼,肯定就是其他線程在**釋放鎖時“喚醒”**被阻塞着的線程去“拿鎖”。

ReentrantLock中的源碼

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;
    }
           

其中,unparkSuccessor(h)方法就是“喚醒操作”,主要流程如代碼所示

  1. 嘗試釋放目前線程持有的鎖
  2. 如果成功釋放,那麼去喚醒頭結點的後繼節點(因為頭節點head是不儲存線程資訊的節點,僅僅是因為資料結構設計上的需要,在資料結構上,這種做法往往叫做“空頭節點連結清單”。對應的就有“非空頭結點連結清單”)

unparkSuccessor(h)的執行流程源碼解析:

private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    //如果head節點的下一個節點它是null或者已經被cancelled了(status>0)
    //那麼就從隊列的尾巴往前找,找到一個最前面的并且狀态不是cancelled的線程
    //至于為什麼要從後往前找,不是從前往後找,誰能跟我說一下,這點我也不知道為什麼
    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);
}
           

至此,申請鎖的“阻塞”和釋放鎖的“喚醒”操作中**“隊列什麼時候進行排隊”、“如何排隊”、“什麼時候移除隊列”、“何時阻塞線程”**、“何時喚醒線程”基本都已經解釋清楚了。

如何實作可重入

加鎖操作會對state字段進行+1操作,這裡需要注意到AQS中很多内部變量的修飾符都是采用的volitale,然後配合CAS操作來保證AQS本身的線程安全(因為AQS自己線程安全,基于它的衍生類才能更好地保證線程安全)。

這裡的state字段就是AQS類中的一個用volitale修飾的int變量,state字段初始化時,值為0。表示目前沒有任何線程持有該鎖。

當一個線程每次獲得該鎖時,值就會在原來的基礎上加1,多次獲鎖就會多次加1(指同一個線程),這裡就是可重入。因為可以同一個線程多次獲鎖,隻是對這個字段的值在原來基礎上加1。

相反unlock操作也就是解鎖操作,實際是是調用AQS的release操作,而每執行一次這個操作,就會對state字段在原來的基礎上減1,當state==0的時候就表示目前線程已經完全釋放了該鎖。那麼就會如上文提到的那樣去調用“喚醒”動作,去把在“線程等待隊列中的線程”叫醒。