AQS的原理及原始碼分析

卡斯特梅的雨傘發表於2021-12-27

AQS是什麼

AQS= volatile修飾的state變數(同步狀態) +FIFO佇列(CLH改善版的虛擬雙向佇列,用於阻塞等待喚醒機制)

佇列裡維護的Node節點主要包含:等待狀態waitStatus,前後指標,等待的執行緒。

AQS是個抽象佇列同步器,是JUC體系中用來構建鎖和其他同步器如 ReentrantLock/CountDownLatch/Semphore的基石。AQS內部通過內建的FIFO先進先出的LCH(虛擬雙向連結串列)佇列來完成執行緒排隊,並通過volatile 修飾的int型別狀態變數來表示持有鎖的狀態。

簡單的說,AQS通過volatile 修飾的int型別狀態變數來表示同步狀態,加volatial的目的是保證可見性。然後如果狀態變數大於等於1是表示資源被佔用,這時候搶不到資源的執行緒就要進入排隊等候佇列,等待資源的釋放,這裡面就需要阻塞等待喚醒機制來實現,AQS通過把等待獲取資源的執行緒封裝為Node<Thread>節點入隊,在資源釋放後通過LockSupport.park().unPark()來喚醒執行緒,通過CAS自旋來進行資源的搶佔。

AQS.png

AQS框架.png

AQS原始碼解析——以ReentrantLock為例

公平鎖與非公平鎖

ReentrantLock預設是非公平鎖,如果要實現公平鎖建構函式中傳入true表示建立的是公平鎖。

公平鎖相較於非公平鎖體現在公平鎖會先判斷佇列中是否有等待的執行緒,有的話優先獲取到鎖資源。

公平鎖與非公平鎖.png

ReentrantLock 類圖

ReentrantLock.png

AQS的Node屬性含義

node節點屬性.png

node節點屬性2.png

AQS結構圖

AQS結構.png

非公平鎖加鎖過程

以3個執行緒分別為ABC爭搶鎖為例。

程式碼示例

public class ReentrantLockTest {
    //模擬銀行排隊
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        //第一個獲取到鎖的客戶,執行自己的業務60秒
        new Thread(() -> {
            lock.lock();
            try {
                System.out.println("A 獲取到鎖,執行任務------------");
                TimeUnit.SECONDS.sleep(60);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "A").start();
        //第二個客戶獲取不到鎖,阻塞
        new Thread(() -> {
            lock.lock();
            try {
                System.out.println("B 獲取到鎖,執行任務------------");
            } finally {
                lock.unlock();
            }
        }, "B").start();
        //第三個客戶獲取不到鎖,阻塞
        new Thread(() -> {
            lock.lock();
            try {
                System.out.println("C 獲取到鎖,執行任務------------");
            } finally {
                lock.unlock();
            }
        }, "C").start();
    }
}
  1. 當A進來加鎖時,會進行CAS加鎖,加鎖成功就會設定exclusiveOwnerThread目前佔用鎖的執行緒為自己。
  2. 當B進來加鎖時,也會進行CAS嘗試加鎖,這時候加鎖不成功後,或呼叫嘗試獲取鎖的方法。這個方法裡或再判斷下這時候鎖狀態是否為0,也就是鎖是否釋放了,如果為0則會再進行CAS嘗試加鎖;如果鎖狀態不為0表示被佔用了,這時候回判斷是否目前加鎖的執行緒是不是自己,如果是的話就進入可重入鎖的邏輯,對加鎖state變數加1,這就是我們加幾次鎖就要減幾次鎖的原因。
  3. 如果B嘗試加鎖失敗後,B就會進入等待佇列中進行等待,在加入佇列的操作中,AQS會把執行緒B先包裝成一個獨佔鎖模式的Node節點,並判斷尾結點是否為空,為空的話就要先判斷佇列是否還未初始化,如果還未初始化,會先建立一個空的哨兵節點(也叫虛節點,主要作用是用來佔位),再將執行緒B的節點與哨兵節點進行雙向佇列關聯,跟在哨兵節點後面,這時候就入隊成功了。如果判斷尾結點不為空,那就設定當前節點為尾結點,並與之前的尾結點設定關聯關係。
  4. 在新增如佇列成功後,執行緒B會呼叫acquireQueued方法繼續嘗試,執行緒B會通過自旋判斷自己在佇列中的位置,如果執行緒B的前節點是哨兵節點,那麼執行緒B進行自旋處理,首先會繼續CAS嘗試加鎖,這時候如果還是不成功,就會設定執行緒B的字首節點的等待狀態從0變成-1,表示等待被喚醒狀態。繼續進入自旋邏輯,還是會再嘗試CAS嘗試加鎖一次,還是失敗就會呼叫LockSupport.park(this);方法把執行緒設定為阻塞狀態,等待被喚醒。
  5. 當執行緒A接收完業務後釋放鎖,釋放鎖時當判斷釋放後state的狀態為0時,就會把當前鎖的狀態設定為0,表示鎖已經空閒了,並設定exclusiveOwnerThread目前佔用鎖的執行緒為null。然後判斷頭結點是否不為空且頭節點的等待狀態為-1等待被喚醒,如果是的話就走喚醒邏輯,先把頭節點等待狀態設定為初始值0,然後判斷頭結點的字尾節點如不為空的話,就喚醒它。這樣子執行緒B就會被喚醒了。
  6. 執行緒B被喚醒後就會繼續進行自旋CAS嘗試獲取鎖,這時候就能成功獲取到了。而獲取到鎖後state狀態繼續變成1表示鎖被佔用,設定exclusiveOwnerThread目前佔用鎖的執行緒為執行緒B。然後把原來執行緒B的節點設定為頭節點,並把B處理為null的哨兵節點,把原來的哨兵節點取消前後指標引用讓GC回收掉。

注意

  • 一個執行緒會嘗試搶4次鎖才會進入到等待喚醒的阻塞狀態中。

AQS為什麼必須有哨兵節點——佔位的目的

1.如果沒有哨兵節點,那麼每次執行入隊操作,都需要判斷head是否為空,如果為空則head=new Node如果不為空則head.next=new Node,而有哨兵節點則可以大膽的head.next=new Node.

2.如果沒有哨兵節點,可能存在之前所說的安全性問題,當只有一個節點的時候執行入隊方法,無法保證last和head不為空。哪怕執行enqueue入隊之前last和head還指向一個節點,可能由於併發性在具體呼叫enqueue方法操作last的時候head和last共同指向的頭節點已經完成出隊,此時last和head都為null,所以enqueue方法中的last.next=new node會拋空指標異常,且由於執行緒併發性的問題,last始終可能隨時為空的問題不使用哨兵節點是無法解決的

相關文章