天天看點

AQS的原理及源碼分析

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源碼解析——以ReentrantLock為例

公平鎖與非公平鎖

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

公平鎖相較于非公平鎖展現在公平鎖會先判斷隊列中是否有等待的線程,有的話優先擷取到鎖資源。

ReentrantLock 類圖

AQS的Node屬性含義

AQS結構圖

非公平鎖加鎖過程

以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始終可能随時為空的問題不使用哨兵節點是無法解決的。