AQS是什么
AQS= volatile修飾的state變量(同步狀態(tài)) +FIFO隊列(CLH改善版的虛擬雙向隊列,用于阻塞等待喚醒機(jī)制)
隊列里維護(hù)的Node節(jié)點(diǎn)主要包含:等待狀態(tài)waitStatus,前后指針,等待的線程。
AQS是個抽象隊列同步器,是JUC體系中用來構(gòu)建鎖和其他同步器如 ReentrantLock/CountDownLatch/Semphore的基石。AQS內(nèi)部通過內(nèi)置的FIFO先進(jìn)先出的LCH(虛擬雙向鏈表)隊列來完成線程排隊,并通過volatile 修飾的int類型狀態(tài)變量來表示持有鎖的狀態(tài)。
簡單的說,AQS通過volatile 修飾的int類型狀態(tài)變量來表示同步狀態(tài),加volatial的目的是保證可見性。然后如果狀態(tài)變量大于等于1是表示資源被占用,這時候搶不到資源的線程就要進(jìn)入排隊等候隊列,等待資源的釋放,這里面就需要阻塞等待喚醒機(jī)制來實(shí)現(xiàn),AQS通過把等待獲取資源的線程封裝為Node<Thread>節(jié)點(diǎn)入隊,在資源釋放后通過LockSupport.park().unPark()來喚醒線程,通過CAS自旋來進(jìn)行資源的搶占。


AQS源碼解析——以ReentrantLock為例
公平鎖與非公平鎖
ReentrantLock默認(rèn)是非公平鎖,如果要實(shí)現(xiàn)公平鎖構(gòu)造函數(shù)中傳入true表示創(chuàng)建的是公平鎖。
公平鎖相較于非公平鎖體現(xiàn)在公平鎖會先判斷隊列中是否有等待的線程,有的話優(yōu)先獲取到鎖資源。

ReentrantLock 類圖

AQS的Node屬性含義


AQS結(jié)構(gòu)圖

非公平鎖加鎖過程
以3個線程分別為ABC爭搶鎖為例。
代碼示例
public class ReentrantLockTest {
//模擬銀行排隊
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
//第一個獲取到鎖的客戶,執(zhí)行自己的業(yè)務(wù)60秒
new Thread(() -> {
lock.lock();
try {
System.out.println("A 獲取到鎖,執(zhí)行任務(wù)------------");
TimeUnit.SECONDS.sleep(60);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "A").start();
//第二個客戶獲取不到鎖,阻塞
new Thread(() -> {
lock.lock();
try {
System.out.println("B 獲取到鎖,執(zhí)行任務(wù)------------");
} finally {
lock.unlock();
}
}, "B").start();
//第三個客戶獲取不到鎖,阻塞
new Thread(() -> {
lock.lock();
try {
System.out.println("C 獲取到鎖,執(zhí)行任務(wù)------------");
} finally {
lock.unlock();
}
}, "C").start();
}
}
- 當(dāng)A進(jìn)來加鎖時,會進(jìn)行CAS加鎖,加鎖成功就會設(shè)置exclusiveOwnerThread目前占用鎖的線程為自己。
- 當(dāng)B進(jìn)來加鎖時,也會進(jìn)行CAS嘗試加鎖,這時候加鎖不成功后,或調(diào)用嘗試獲取鎖的方法。這個方法里或再判斷下這時候鎖狀態(tài)是否為0,也就是鎖是否釋放了,如果為0則會再進(jìn)行CAS嘗試加鎖;如果鎖狀態(tài)不為0表示被占用了,這時候回判斷是否目前加鎖的線程是不是自己,如果是的話就進(jìn)入可重入鎖的邏輯,對加鎖state變量加1,這就是我們加幾次鎖就要減幾次鎖的原因。
- 如果B嘗試加鎖失敗后,B就會進(jìn)入等待隊列中進(jìn)行等待,在加入隊列的操作中,AQS會把線程B先包裝成一個獨(dú)占鎖模式的Node節(jié)點(diǎn),并判斷尾結(jié)點(diǎn)是否為空,為空的話就要先判斷隊列是否還未初始化,如果還未初始化,會先創(chuàng)建一個空的哨兵節(jié)點(diǎn)(也叫虛節(jié)點(diǎn),主要作用是用來占位),再將線程B的節(jié)點(diǎn)與哨兵節(jié)點(diǎn)進(jìn)行雙向隊列關(guān)聯(lián),跟在哨兵節(jié)點(diǎn)后面,這時候就入隊成功了。如果判斷尾結(jié)點(diǎn)不為空,那就設(shè)置當(dāng)前節(jié)點(diǎn)為尾結(jié)點(diǎn),并與之前的尾結(jié)點(diǎn)設(shè)置關(guān)聯(lián)關(guān)系。
- 在添加如隊列成功后,線程B會調(diào)用
acquireQueued方法繼續(xù)嘗試,線程B會通過自旋判斷自己在隊列中的位置,如果線程B的前節(jié)點(diǎn)是哨兵節(jié)點(diǎn),那么線程B進(jìn)行自旋處理,首先會繼續(xù)CAS嘗試加鎖,這時候如果還是不成功,就會設(shè)置線程B的前綴節(jié)點(diǎn)的等待狀態(tài)從0變成-1,表示等待被喚醒狀態(tài)。繼續(xù)進(jìn)入自旋邏輯,還是會再嘗試CAS嘗試加鎖一次,還是失敗就會調(diào)用LockSupport.park(this);方法把線程設(shè)置為阻塞狀態(tài),等待被喚醒。 - 當(dāng)線程A接收完業(yè)務(wù)后釋放鎖,釋放鎖時當(dāng)判斷釋放后state的狀態(tài)為0時,就會把當(dāng)前鎖的狀態(tài)設(shè)置為0,表示鎖已經(jīng)空閑了,并設(shè)置exclusiveOwnerThread目前占用鎖的線程為null。然后判斷頭結(jié)點(diǎn)是否不為空且頭節(jié)點(diǎn)的等待狀態(tài)為-1等待被喚醒,如果是的話就走喚醒邏輯,先把頭節(jié)點(diǎn)等待狀態(tài)設(shè)置為初始值0,然后判斷頭結(jié)點(diǎn)的后綴節(jié)點(diǎn)如不為空的話,就喚醒它。這樣子線程B就會被喚醒了。
- 線程B被喚醒后就會繼續(xù)進(jìn)行自旋CAS嘗試獲取鎖,這時候就能成功獲取到了。而獲取到鎖后state狀態(tài)繼續(xù)變成1表示鎖被占用,設(shè)置exclusiveOwnerThread目前占用鎖的線程為線程B。然后把原來線程B的節(jié)點(diǎn)設(shè)置為頭節(jié)點(diǎn),并把B處理為null的哨兵節(jié)點(diǎn),把原來的哨兵節(jié)點(diǎn)取消前后指針引用讓GC回收掉。
注意
- 一個線程會嘗試搶4次鎖才會進(jìn)入到等待喚醒的阻塞狀態(tài)中。
AQS為什么必須有哨兵節(jié)點(diǎn)——占位的目的
1.如果沒有哨兵節(jié)點(diǎn),那么每次執(zhí)行入隊操作,都需要判斷head是否為空,如果為空則head=new Node如果不為空則head.next=new Node,而有哨兵節(jié)點(diǎn)則可以大膽的head.next=new Node.
2.如果沒有哨兵節(jié)點(diǎn),可能存在之前所說的安全性問題,當(dāng)只有一個節(jié)點(diǎn)的時候執(zhí)行入隊方法,無法保證last和head不為空。哪怕執(zhí)行enqueue入隊之前l(fā)ast和head還指向一個節(jié)點(diǎn),可能由于并發(fā)性在具體調(diào)用enqueue方法操作last的時候head和last共同指向的頭節(jié)點(diǎn)已經(jīng)完成出隊,此時last和head都為null,所以enqueue方法中的last.next=new node會拋空指針異常,且由于線程并發(fā)性的問題,last始終可能隨時為空的問題不使用哨兵節(jié)點(diǎn)是無法解決的。