AQS(AbstractQueuedSynchronizer)抽象隊列同步器及工作原理解析
前言
AQS 絕對是JUC的重要基石,也是面試中經(jīng)常被問到的,所以我們要搞清楚這個AQS到底是什么?騎工作原理是什么?
AQS是什么?
AQS,AbstractQueuedSynchronizer,即隊列同步器。它是構建鎖或者其他同步組件的基礎框架(如ReentrantLock、ReentrantReadWriteLock、Semaphore等),JUC并發(fā)包的作者(Doug Lea)期望它能夠成為實現(xiàn)大部分同步需求的基礎。它是JUC并發(fā)包中的核心基礎組件,相比synchronized,synchronized缺少了獲取鎖與釋放鎖的可操作性,可中斷、超時獲取鎖。
是用來構建鎖或者其他同步器組件的重量級基礎框架及整個JUC體系的基石,通過內置的FIFO對列來完成資源獲取線程的排隊工作,并通過一個int類型變量表示持有鎖的狀態(tài)。

CLH隊列:CLH(Craig, Landin, and Hagersten)隊列是一個虛擬的雙向隊列(虛擬的雙向隊列即不存在隊列實例,僅存在結點之間的關聯(lián)關系)。AQS 是將每條請求共享資源的線程封裝成一個 CLH 鎖隊列的一個結點(Node)來實現(xiàn)鎖的分配。通過CAS完成對State值的修改。
同步對列的內部結構及繼承關系


用銀行辦理業(yè)務的案例模擬AQS如何進行線程管理和通知機制
1、初始化的時候,state = 0 (0 表示沒有人,1表示有人),線程池也有沒有人在執(zhí)行,現(xiàn)在就是沒有顧客的時候,因此第一個線程去的時候都是公平鎖狀態(tài)直接到窗口辦理業(yè)務。

2、現(xiàn)在有一個線程Thread A進來那么就直接到了業(yè)務窗口去辦理,并且通過CAS將state的值變成1。

3、此時有一個線程Thread B 進來,通過getStatue() 方法查看到state = 1,此時ThreadA有在占用著,所以現(xiàn)在ThreadB線程就必須先入隊等待ThreadA結束后再調用,但是由于現(xiàn)在隊列是空的,所以ThreadB線程并不會馬上進入到隊列,他會先進入addWaiter() 方法到enq()這個方法中去,這個方法實質上就是一個自旋鎖,在這個方法中主要的時候先要實現(xiàn)一個隊列的初始化工作,先形成一個傀儡結點(哨兵結點)起到一個占位的作用,然后才能將ThreadB線程掛在后面。

部分源碼附上:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;// 將tail的前指針賦值過去
if (pred != null) { //一開始的時候并沒有指向所以位null,因此條件不成立不走里面的語句
node.prev = pred;
if (compareAndSetTail(pred, node)) {//CAS
pred.next = node;
return node;
}
}
enq(node);//一開始會來走這個enq()這個方法。
return node;
}
因為當前的ThreadB線程進入時,tail的prev指針為null所以就會進enq()方法。如下:
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
4、ThreadC線程開始入隊,那么這個就是跟ThreadB是一樣的,只不過是說沒有了初始化那步了,直接掛在線程ThreadB上即可。

5、ThreadB 會去嘗試acquireQueued()這個方法,那么第一次的時候會將哨兵結點的waitStatus = -1; 然后繼續(xù)自旋,進入到if條件的下一個條件中被調用park() 阻塞掉,等待著ThreadA 線程的unpark()。這樣才算真正的入列等待了。

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()) //第一次的時候會阻塞在前面的條件并將哨兵的waitState置為-1,第二次的話第一個條件為真,所以會走第二個判斷條件,會將訪問的線程給阻塞掉等待unpark();
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); //將訪問次方法的線程給阻塞掉,等待unpark()方法喚醒
return Thread.interrupted();
}
6、當ThreadA線程辦好業(yè)務的時候那么就會調用unlock()方法釋放鎖,unlock() 方法中又會調用sync.release()方法,并將當前窗口的線程變成null,state 置成0,通過調用 unparkSuccessor()方法將 傀儡結點的waitState = 0。

private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
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); //釋放鎖喚醒等待隊列中的等待線程
}7、接著會調用unpark()方法將在前面等待的ThreadB線程給喚醒去窗口辦業(yè)務。將head結點指向ThreadB結點,ThreadB的prev結點為null,next結點也為null,ThreadB變成空結點,此節(jié)點就成了新的哨兵結點,原來的哨兵結點被GC回收。

8、ThreadC線程出列和上面的過程是一樣的。
結語
AQS的核心大致是這樣,如果說我講的你還是沒有聽懂的話,可以去B站看善硅谷陽哥的高頻面試題那集去看看。我只是將其做了一個簡化,用自己的話把這個過程復述出來,希望能對你有所幫助。
到此這篇關于AQS(AbstractQueuedSynchronizer)抽象隊列同步器的文章就介紹到這了,更多相關AQS抽象隊列同步器內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
解析SpringSecurity自定義登錄驗證成功與失敗的結果處理問題
這篇文章主要介紹了SpringSecurity系列之自定義登錄驗證成功與失敗的結果處理問題,本文通過實例給大家講解的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2019-11-11
SpringBoot實現(xiàn)elasticsearch 查詢操作(RestHighLevelClient 
這篇文章主要給大家介紹了SpringBoot如何實現(xiàn)elasticsearch 查詢操作,文中有詳細的代碼示例和操作流程,具有一定的參考價值,需要的朋友可以參考下2023-07-07
SpringBoot下獲取resources目錄下文件的常用方法
本文詳細介紹了SpringBoot獲取resources目錄下文件的常用方法,包括使用this.getClass()方法、ClassPathResource獲取以及hutool工具類ResourceUtil獲取,感興趣的可以了解一下2024-10-10
Spring boot實現(xiàn)一個簡單的ioc(1)
這篇文章主要為大家詳細介紹了Spring boot實現(xiàn)一個簡單的ioc,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-04-04

