死磕java concurrent包系列(二)基於ReentrantLock理解AQS同步佇列的細節和設計模式

lyowish發表於2018-12-03

前言

之前介紹過併發問題的解決方式就是一般通過鎖,concurrent包中最重要的介面就是lock介面,它可以顯示的獲取或者釋放鎖,對於lock介面來說最常見的實現就是ReetrantLock(可重入鎖),而ReetrantLock的實現又離不開AQS。

AQS是concurrent包中最核心的併發元件,在讀本文之前建議先閱讀:

https://juejin.im/post/5c021da16fb9a049e65ffcbf 徹底理解CAS機制,因為CAS在整個ReetrantLock中隨處可見,它是lock的基礎。

網上有許多類似文章,但是這一部分的東西比較抽象,需要不斷理解,本文將基於原始碼分析concurrent包的最核心的元件AQS,將不好理解的部分儘量用圖片來分析徹底理解ReetrantLock的原理

這部分是concurrent包的核心,理解了之後再去理解SemaPhore LinkedBlockingQueue ArrayBlockingQueue 等就信手拈來了,所以會花比較多的篇幅

重入鎖ReetrantLock

Lock介面

先大概看一看lock介面

public interface Lock {
    // 加鎖
    void lock();
    // 可中斷獲取鎖,獲取鎖的過程中可以中斷。
    void lockInterruptibly() throws InterruptedException;
    //立即返回的獲取鎖,返回true表示成功,false表示失敗
    boolean tryLock();
    //根據傳入時間立即返回的獲取鎖,返回true表示成功,false表示失敗
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    //解鎖
    void unlock();
    //獲取等待的條件佇列(之後會詳細分析)
    Condition newCondition();
}複製程式碼

而我們一般使用ReetrantLock:

Lock lock = new ReentrantLock(); 
lock.lock(); 
try{ 
 //業務程式碼...... 
}finally{ 
 lock.unlock(); 
}複製程式碼

它在使用上是比較簡單的,在正式分析之前我們先看看什麼是公平鎖和非公平鎖

公平鎖和非公平鎖

公平鎖是指多個執行緒按照申請鎖的順序來獲取鎖,執行緒直接進入FIFO佇列,佇列中的第一個執行緒才能獲得鎖。

用一個打水的例子來理解:

死磕java concurrent包系列(二)基於ReentrantLock理解AQS同步佇列的細節和設計模式


公平鎖的優點是等待鎖的執行緒不會夯死。缺點是吞吐效率相對非公平鎖要低,等待佇列中除第一個執行緒以外的所有執行緒都會阻塞,CPU喚醒阻塞執行緒的開銷比非公平鎖大。


非公平鎖是多個執行緒加鎖時直接嘗試獲取鎖,獲取不到才會到等待佇列的隊尾等待。但如果此時鎖剛好可用,那麼這個執行緒可以無需阻塞直接獲取到鎖,所以非公平鎖有可能出現後申請鎖的執行緒先獲取鎖的場景。

死磕java concurrent包系列(二)基於ReentrantLock理解AQS同步佇列的細節和設計模式

非公平鎖的優點是可以減少喚起執行緒的開銷(因為可能有的執行緒可以直接獲取到鎖,CPU也就不用喚醒它),所以整體的吞吐效率高。缺點是處於等待佇列中的執行緒可能會夯死(試想恰好每次有新執行緒來,它恰巧都每次獲取到鎖,此時還在排隊等待獲取鎖的執行緒就悲劇了),或者等很久才會獲得鎖。

總結

公平鎖和非公平鎖的差異在於是否按照申請鎖的順序來獲取鎖,非公平鎖可能會出現有多個執行緒等待時,有一個人品特別的好的執行緒直接沒有等待而直接獲取到了鎖的情況,他們各有利弊;ReetrantLock在構造時預設是非公平的,可以通過引數控制。

ReetrantLock與AQS

這裡以ReentrantLock為例,簡單講解ReentrantLock與AQS的關係

死磕java concurrent包系列(二)基於ReentrantLock理解AQS同步佇列的細節和設計模式

死磕java concurrent包系列(二)基於ReentrantLock理解AQS同步佇列的細節和設計模式

從上圖我們可以總結:

  1. ReetrantLock:實現了lock介面,它的內部類有Sync、NonfairSync、FairSync(他們三個是繼承了AQS),建立構造ReetrantLock時可以指定是非公平鎖(NonfairSync)還是公平鎖(FairSync)
  2. Sync:他是抽象類,也是ReetrantLock內部類,實現了tryRelease方法,tryAccquire方法由它的子類NonfairSync、FairSync自己實現。
  3. AQS:它是一個抽象類,但是值得注意的是它程式碼中卻沒有一個抽象方法,其中獲取鎖(tryAcquire方法)和釋放鎖(tryRelease方法)也沒有提供預設實現,需要子類重寫這兩個方法實現具體邏輯(典型的模板方法設計模式)。
  4. Node:AQS的內部類,本質上是一個雙向連結串列,用來管理獲取鎖的執行緒(後續詳細解讀)

這樣設計的結構有幾點好處:

1. 首先為什麼要有Sync這個內部類呢?

 因為無論是NonfairSync還是FairSync,他們解鎖的過程是一樣的,不同只是加鎖的過程,Sync提供加鎖的模板方法讓子類自行實現

2. AQS為什麼要宣告為Abstract,內部卻沒有任何abstract方法?

這是因為AQS只是作為一個基礎元件,從上圖可以看出countDownLatch,Semaphore等併發元件都依賴了它,它並不希望直接作為直接操作類對外輸出,而更傾向於作為一個基礎併發元件,為真正的實現類提供基礎設施,例如構建同步佇列,控制同步狀態等。

AQS是採用模板方法的設計模式,它作為基礎組併發件,封裝了一層核心併發操作(比如獲取資源成功後封裝成Node加入佇列,對佇列雙向連結串列的處理),但是實現上分為兩種模式,即共享模式(如Semaphore)與獨佔模式(如ReetrantLock,這兩個模式的本質區別在於多個執行緒能不能共享一把鎖),而這兩種模式的加鎖與解鎖實現方式是不一樣的,但AQS只關注內部公共方法實現並不關心外部不同模式的實現,所以提供了模板方法給子類使用:例如:

ReentrantLock需要自己實現tryAcquire()方法和tryRelease()方法,而實現共享模式的Semaphore,則需要實現tryAcquireShared()方法和tryReleaseShared()方法,這樣做的好處?因為無論是共享模式還是獨佔模式,其基礎的實現都是同一套元件(AQS),只不過是加鎖解鎖的邏輯不同罷了,更重要的是如果我們需要自定義鎖的話,也變得非常簡單,只需要選擇不同的模式實現不同的加鎖和解鎖的模板方法即可。

總結

ReetrantLock:實現了lock介面,內部類有Sync、NonfairSync、FairSync(他們三個是繼承了AQS)這裡用了模板方法的設計模式。

Node和AQS工作原理

之前介紹AQS是提供基礎設施,如構建同步佇列,控制同步狀態等,它的工作原理是怎樣的呢?

我們先看看AQS類中幾個重要的欄位:

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer{
//指向同步佇列隊頭
private transient volatile Node head;

//指向同步的隊尾
private transient volatile Node tail;

//同步狀態,0代表鎖未被佔用,1代表鎖已被佔用
private volatile int state;

//省略.
}
複製程式碼

再看看Node這個內部類:它是對每一個訪問同步程式碼塊的執行緒的封裝

關於等待狀態,我們暫時只需關注SIGNAL 和初始化狀態即可

死磕java concurrent包系列(二)基於ReentrantLock理解AQS同步佇列的細節和設計模式

AQS本質上就是由node構成的雙向連結串列,內部有node head和node tail。

AQS通過定義的state欄位來控制同步狀態,當state=0時,說明沒有鎖資源被站東,當state=1時,說明有執行緒目前正在使用鎖的資源,這個時候其他執行緒必須加入同步佇列進行等待;

既然要加入佇列,那麼AQS是內部通過內部類Node構成FIFO的同步佇列實現執行緒獲取鎖排隊,同時利用內部類ConditionObject構建條件佇列,當呼叫condition.wait()方法後,執行緒將會加入條件佇列中,而當呼叫signal()方法後,執行緒將從條件佇列移動到同步佇列中進行鎖競爭。注意這裡涉及到兩種佇列,一種的同步佇列,當鎖資源已經被佔用,而又有執行緒請求鎖而等待的後將加入同步佇列等待,而另一種則是條件佇列(可有多個),通過Condition呼叫await()方法釋放鎖後,將加入等待佇列。

條件佇列可以暫時先放一邊,下一節再詳細分析,因為當我們呼叫ReetrantLock.lock()方法時,實際操作的是基於node結構的同步佇列,此時AQS中的state變數則是代表同步狀態,加鎖後,如果此時state的值為0,則說明當前執行緒可以獲取到鎖,同時將state設定為1,表示獲取成功。如果呼叫ReetrantLock.lock()方法時state已為1,也就是當前鎖已被其他執行緒持有,那麼當前執行執行緒將被封裝為Node結點加入同步佇列等待。

死磕java concurrent包系列(二)基於ReentrantLock理解AQS同步佇列的細節和設計模式

總結

如上圖所示為AQS的同步佇列模型;

  1. AQS內部有一個由Node組成的同步佇列,它是雙向連結串列結構
  2. AQS內部通過state來控制同步狀態,當執行lock時,如果state=0時,說明沒有任何執行緒佔有共享資源的鎖,此時執行緒會獲取到鎖並把state設定為1;當state=1時,則說明有執行緒目前正在使用共享變數,其他執行緒必須加入同步佇列進行等待.
AQS內部分為共享模式(如Semaphore)和獨佔模式(如ReetrantLock),無論是共享模式還是獨佔模式的實現類,都維持著一個虛擬的同步佇列,當請求鎖的執行緒超過現有模式的限制時,會將執行緒包裝成Node結點並將執行緒當前必要的資訊儲存到node結點中,然後加入同步佇列等會獲取鎖,而這系列操作都有AQS協助我們完成,這也是作為基礎元件的原因,無論是Semaphore還是ReetrantLock,其內部絕大多數方法都是間接呼叫AQS完成的。

接下來我們看詳細實現

基於ReetrantLock分析AQS獨佔鎖模式的實現

非公平鎖的獲取鎖

AQS的實現依賴於內部的同步佇列(就是一個由node構成的FIFO的雙向連結串列對列)來完成對同步狀態(state)的管理,當前執行緒獲取鎖失敗時,AQS會將該執行緒封裝成一個Node並將其加入同步佇列,同時會阻塞當前執行緒,當同步資源釋放時,又會將頭結點head中的執行緒喚醒,讓其嘗試獲取同步狀態。這裡從ReetrantLock入手分析AQS的具體實現,我們先以非公平鎖為例進行分析。

獲取鎖

來看ReetrantLock的原始碼:

//預設構造,建立非公平鎖NonfairSync
public ReentrantLock() {
    sync = new NonfairSync();
}
//根據傳入引數建立鎖型別
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

//加鎖操作
public void lock() {
     sync.lock();
}

複製程式碼

這裡說明ReetrantLock預設構造方法就是構造一個非公平鎖,呼叫lock方法時候:

/**
 * 非公平鎖實現
 */
static final class NonfairSync extends Sync {
    //加鎖
    final void lock() {
        //執行CAS操作,本質就是CAS更新state:
        //判斷state是否為0,如果為0則把0更新為1,並返回true否則返回false
        if (compareAndSetState(0, 1))
       //成功則將獨佔鎖執行緒設定為當前執行緒  
          setExclusiveOwnerThread(Thread.currentThread());
        else
            //否則再次請求同步狀態
            acquire(1);
    }
}

複製程式碼

也就是說,通過CAS機制保證併發的情況下只有一個執行緒可以成功將state設定為1,獲取到鎖;

此時,其它執行緒在執行compareAndSetState時,因為state此時不是0,所以會失敗並返回false,執行acquire(1);

加入同步佇列

public final void acquire(int arg) {
    //再次嘗試獲取同步狀態
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

複製程式碼

這裡傳入引數arg是state的值,因為要獲取鎖,而status為0時是釋放鎖,1則是獲取鎖,所以這裡一般傳遞引數為1,進入方法後首先會執行tryAcquire(1)方法,在前面分析過該方法在AQS中並沒有具體實現,而是交由子類實現,因此該方法是由ReetrantLock內部類實現

//NonfairSync類
static final class NonfairSync extends Sync {

    protected final boolean tryAcquire(int acquires) {
         return nonfairTryAcquire(acquires);
     }
 }
複製程式碼

死磕java concurrent包系列(二)基於ReentrantLock理解AQS同步佇列的細節和設計模式

假設有三個執行緒:執行緒1已經獲得到了鎖,執行緒2正在同步佇列中排隊,此時執行緒3執行lock方法嘗試獲取鎖的時,執行緒1正好釋放了鎖,將state更新為0,那麼執行緒3就可能線上程2還沒有被喚醒之前去獲取到這個鎖。

如果此時還沒有獲取到鎖(nonfairTryAcquire返回false),那麼接下來會把該執行緒封裝成node去同步佇列裡排隊,程式碼層面上執行的是acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

ReetrantLock為獨佔鎖,所以傳入的引數為Node.EXCLUSIVE

private Node addWaiter(Node mode) {
    //將請求同步狀態失敗的執行緒封裝成結點
    Node node = new Node(Thread.currentThread(), mode);

    Node pred = tail;
    //如果是第一個結點加入肯定為空,跳過。
    //如果非第一個結點則直接執行CAS入隊操作,嘗試在尾部快速新增
    if (pred != null) {
        node.prev = pred;
        //使用CAS執行尾部結點替換,嘗試在尾部快速新增
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //如果第一次加入或者CAS操作沒有成功執行enq入隊操作
    enq(node);
    return node;
}
複製程式碼

其中tail是AQS的成員變數,指向隊尾(這點前面的我們分析過AQS維持的是一個雙向的連結串列結構同步佇列),如果第一次獲取到鎖,AQS還沒有初始化,則為tail肯定為空,那麼將執行enq(node)操作,如果非第一個結點即tail指向不為null,直接嘗試執行CAS操作加入隊尾(再一次使用CAS操作實現執行緒安全),如果CAS操作失敗或第一次加入同步佇列還是會執行enq(node),繼續看enq(node):

private Node enq(final Node node) {
    //死迴圈
    for (;;) {
         Node t = tail;
         //如果佇列為null,即沒有頭結點
         if (t == null) { // Must initialize
             //建立並使用CAS設定頭結點
             if (compareAndSetHead(new Node()))
                 tail = head;
         } else {//隊尾新增新結點
             node.prev = t;
             if (compareAndSetTail(t, node)) {
                 t.next = node;
                 return t;
             }
         }
     }
}
複製程式碼

這個方法使用一個死迴圈進行CAS操作,可以解決多執行緒併發問題。這裡做了兩件事:

一是佇列不存在的建立新結點並初始化tail、head:使用compareAndSetHead設定頭結點,head和tail都指向head。

二是佇列已存在,則將新結點node新增到隊尾。

注意addWaiterenq這兩個方法都存在同樣的程式碼將執行緒設定為同步佇列的隊尾:

             node.prev = t;
             if (compareAndSetTail(t, node)) {
                 t.next = node;
                 return t;
             }複製程式碼

這是因為,在多執行緒環境中,假設執行緒1、2、3、4同時執行addWaiter()方法入隊,而此時頭節點不為null,那麼他們會同時執行addWaiter中的compareAndSetTail方法將隊尾指向它,新增到隊尾。

但這個時候CAS操作保證只有一個可以成功,假設此時執行緒1成功新增到隊尾,那麼執行緒2、3、4執行CAS都會失敗,那麼執行緒2、3、4會在enq這個方法內部死迴圈執行compareAndSetTail方法將隊尾指向它,直到成功新增到隊尾為止。enq這個方法在內部對併發情況下進行補償。

自旋

回到之前的acquire()方法,新增到同步佇列後,結點就會進入一個自旋過程,自旋的意思就是原地轉圈圈:即結點都在觀察時機準備獲取同步狀態,自旋過程是在acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法中執行的,先看前半部分

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        //自旋,死迴圈
        for (;;) {
            //獲取前結點
            final Node p = node.predecessor();
            當且僅當p為頭結點才嘗試獲取同步狀態,FIFO
            if (p == head && tryAcquire(arg)) {
                //此時當前node前驅節點為head且已經tryAcquire獲取到了鎖,正在執行了它的相關資訊
                //已經沒有任何用處了,所以現在需要考慮將它GC掉
                //將node設定為頭結點
                setHead(node);
                //清空原來頭結點的引用便於GC
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            //如果前驅結點不是head,判斷是否掛起執行緒
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;       
        }
    } finally {
        if (failed)
            //最終都沒能獲取同步狀態,結束該執行緒的請求
            cancelAcquire(node);
    }
}
複製程式碼

//設定為頭結點
private void setHead(Node node) {
        head = node;
        //清空結點資料以便於GC
        node.thread = null;
        node.prev = null;
}
複製程式碼

死迴圈中,如果滿足了if (p == head && tryAcquire(arg))

如下圖,會執行sethead方法:

死磕java concurrent包系列(二)基於ReentrantLock理解AQS同步佇列的細節和設計模式

當然如果前驅結點不是head而它又沒有獲取到鎖,那麼執行如下:

//如果前驅結點不是head,判斷是否掛起執行緒
if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())

      interrupted = true;
}

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //獲取當前結點的等待狀態
        int ws = pred.waitStatus;
        //如果為等待喚醒(SIGNAL)狀態則返回true
        if (ws == Node.SIGNAL)
            return true;
        //如果ws>0 則說明是結束狀態,
        //遍歷前驅結點直到找到沒有結束狀態的結點
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //如果ws小於0又不是SIGNAL狀態,
            //則將其設定為SIGNAL狀態,代表該結點的執行緒正在等待喚醒。
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

private final boolean parkAndCheckInterrupt() {
        //將當前執行緒掛起,執行緒會阻塞住
        LockSupport.park(this);
        //獲取執行緒中斷狀態,interrupted()是判斷當前中斷狀態,
        //並非中斷執行緒,因此可能true也可能false,並返回
        return Thread.interrupted();
}

複製程式碼

這段程式碼有個設計比較好的點:

通常我們在設計佇列時,我們需要考慮如何最大化的減少後續排隊節點對於CPU的消耗,而在AQS中,只要當前節點的前驅節點不是頭結點,再把當前節點加到佇列後就會執行LockSupport.park(this);將當前執行緒掛起,這樣可以最大程度減少CPU消耗。

總結:

是不是還是有點一頭霧水?

沒關係,為了方便理解:我們假設ABC三個執行緒現在同時去獲取鎖,A首先獲取到鎖後一直不釋放,BC加入佇列。那麼對於AQS的同步佇列結構是如何變化的呢?

1、A直接獲取到鎖:

程式碼執行路徑:

(ReetranLock.lock()-> compareAndSetState(0, 1) -> setExclusiveOwnerThread(Thread.currentThread())

此時AQS結構還沒有初始化:

死磕java concurrent包系列(二)基於ReentrantLock理解AQS同步佇列的細節和設計模式

2、B嘗試獲取鎖:

因為A存在把state設定為1,所以B獲取鎖失敗,進行入隊操作加入同步佇列,入隊時發現AQS還沒有初始化(AQS中的tail為null),會在入隊前初始化程式碼在enq方法的死迴圈中:

死磕java concurrent包系列(二)基於ReentrantLock理解AQS同步佇列的細節和設計模式

初始化之後改變tail的prev指向,把自己加到隊尾:

死磕java concurrent包系列(二)基於ReentrantLock理解AQS同步佇列的細節和設計模式

接著會執行acquireQueued方法:

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}複製程式碼

第一次執行:發現自己前序節點是head節點,於是乎再次嘗試獲取鎖,獲取失敗後再shouldParkAfterFailedAcquire方法中把前序節點設定為Singal狀態

第二次執行:再次嘗試獲取鎖,但因為前序節點是Signal狀態了,所以執行parkAndCheckInterrupt把自己休眠起來進行自旋

死磕java concurrent包系列(二)基於ReentrantLock理解AQS同步佇列的細節和設計模式

3、C嘗試獲取鎖:

C獲取鎖和B完全一樣,不同的是它的前序節點是B,所以它並不會一直嘗試獲取鎖,只會呆在B後面park住

死磕java concurrent包系列(二)基於ReentrantLock理解AQS同步佇列的細節和設計模式

AQS通過最簡單的CAS和LockSupport的park,設計出了高效的佇列模型和機制:

1、AQS結構其實是在第二個執行緒獲取鎖的時候再初始化的,就是lazy-Init的思想,最大程度減少不必要的程式碼執行的開銷

2、為了最大程度上提升效率,儘量避免執行緒間的通訊,採用了雙向連結串列的Node結構去儲存執行緒

3、為了最大程度上避免CPU上下文切換執行的消耗,在設計排隊執行緒時,只有頭結點的下一個的執行緒在一直重複執行獲取鎖,佇列後面的執行緒會通過LockSupport進行休眠。

非公平鎖的釋放鎖

上程式碼:

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

//AQS類的release()方法
public final boolean release(int arg) {
    //嘗試釋放鎖
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            //喚醒後繼結點的執行緒
            unparkSuccessor(h);
        return true;
    }
    return false;
}

//ReentrantLock類中的內部類Sync實現的tryRelease(int releases) 
protected final boolean tryRelease(int releases) {

      int c = getState() - releases;
      if (Thread.currentThread() != getExclusiveOwnerThread())
          throw new IllegalMonitorStateException();
      boolean free = false;
      //判斷狀態是否為0,如果是則說明已釋放同步狀態
      if (c == 0) {
          free = true;
          //設定Owner為null
          setExclusiveOwnerThread(null);
      }
      //設定更新同步狀態
      setState(c);
      return free;
  }

複製程式碼

一句話總結:釋放鎖首先就是把volatile型別的變數state減1。state從1變成0.

unparkSuccessor(h)的作用的喚醒後續的節點:

private void unparkSuccessor(Node node) {
    //這裡,node是head節點。
    int ws = node.waitStatus;
    if (ws < 0)//置零當前執行緒所在的結點狀態,允許失敗。
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;//找到下一個需要喚醒的結點s
    if (s == null || s.waitStatus > 0) {//如果為空或已取消
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)//從這裡可以看出,<=0的結點,都是還有效的結點。
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);//喚醒
}

複製程式碼

從程式碼執行操作來看,這裡主要作用是用unpark()喚醒同步佇列中最前邊未放棄執行緒(也就是狀態為CANCELLED的執行緒結點s)。此時,回憶前面分析進入自旋的函式acquireQueued(),s結點的執行緒被喚醒後,會進入acquireQueued()函式的if (p == head && tryAcquire(arg))的判斷,然後s把自己設定成head結點,表示自己已經獲取到資源了,最終acquire()也返回了,這就是獨佔鎖釋放的過程。 

總結

回到之前的圖:A B C三個執行緒獲取鎖,A已經獲取到了鎖,BC在佇列裡面,此時A釋放鎖

死磕java concurrent包系列(二)基於ReentrantLock理解AQS同步佇列的細節和設計模式

執行b.unpark,B被喚醒後繼續執行

if (p == head && tryAcquire(arg))
複製程式碼

因為B的前序節點是head,所以會執行tryAcquire方法嘗試獲取鎖,獲取到鎖之後執行setHead方法把自己設定為頭節點,並且把之前的頭結點也就是上圖中的new Node()設定為null以便於GC:

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                //獲取到鎖之後將當前node設定為頭結點 head指向當前節點node
                setHead(node);
                //p.next就是之前的頭結點,它沒有用了,所以把它gc掉
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}複製程式碼

死磕java concurrent包系列(二)基於ReentrantLock理解AQS同步佇列的細節和設計模式

總之,AQS內部有一個同步佇列,執行緒獲取同步狀態失敗之後會被封裝成node通過park進行自旋,而在釋放同步狀態時,通過unpark進行喚醒後面一個執行緒,讓後面執行緒得以繼續獲取鎖。

ReetrantLock中的公平鎖

瞭解完ReetrantLock中非公平鎖的實現後,我們再來看看公平鎖。與非公平鎖不同的是,在獲取鎖的時,公平鎖的獲取順序是完全遵循時間上的FIFO規則,也就是說先請求的執行緒一定會先獲取鎖,後來的執行緒肯定需要排隊。

下面比較一下公平鎖和非公平鎖lock方法: 

死磕java concurrent包系列(二)基於ReentrantLock理解AQS同步佇列的細節和設計模式

再比較一下公平鎖和非公平鎖lock方法:tryAcquire方法:

死磕java concurrent包系列(二)基於ReentrantLock理解AQS同步佇列的細節和設計模式

死磕java concurrent包系列(二)基於ReentrantLock理解AQS同步佇列的細節和設計模式

唯一的差別就是hasQueuedPredecessors()判斷同步佇列是否存在結點,這就是非公平鎖與公平鎖最大的區別,即公平鎖線上程請求到來時先會判斷同步佇列是否存在結點,如果存在先執行同步佇列中的結點執行緒,當前執行緒將封裝成node加入同步佇列等待。而非公平鎖呢,當執行緒請求到來時,不管同步佇列是否存線上程結點,直接上去嘗試獲取同步狀態,獲取成功直接訪問共享資源,但請注意在絕大多數情況下,非公平鎖才是我們理想的選擇,畢竟從效率上來說非公平鎖總是勝於公平鎖。

總結

以上便是ReentrantLock的內部實現原理,這裡我們簡單進行小結,重入鎖ReentrantLock,是一個基於AQS併發框架的併發控制類,其內部實現了3個類,分別是Sync、NoFairSync以及FairSync類,其中Sync繼承自AQS,實現了釋放鎖的模板方法tryRelease(int),而NoFairSync和FairSync都繼承自Sync,實現各種獲取鎖的方法tryAcquire(int)。

ReentrantLock的所有方法實現幾乎都間接呼叫了這3個類,因此當我們在使用ReentrantLock時,大部分使用都是在間接呼叫AQS同步器中的方法。

AQS在設計時將效能優化到了極致,具體體現在同步佇列的park和unpark,初始化AQS時的懶載入,以及執行緒之間通過Node這樣的資料結構從而避免執行緒間通訊造成的額外開銷,這種由釋放鎖的執行緒主動喚醒後續執行緒的方式也是我們再實際過程中可以借鑑的。

AQS還不止於同步佇列,接下來我們會繼續探討AQS的條件佇列



相關文章