JUC多线程:AQS抽象队列同步器原理
一、AQS 的工作原理:
1.1、什么是 AQS:
????????AQS,Abstract Queued Synchronizer,抽象隊列同步器,是 J.U.C 中實現鎖及同步組件的基礎。工作原理就是如果被請求的共享資源空閑,則將當前請求資源的線程設置為有效的工作線程,并且將共享資源設置為鎖定狀態,如果被請求的共享資源被占用,那么就將獲取不到鎖的線程加入到等待隊列中。這時,就需要一套線程阻塞等待以及被喚醒時的鎖分配機制,而 AQS 是通過 CLH 隊列實現鎖分配的機制。
1.2、CLH 同步隊列的模型:
????????CLH 隊列是由內部類 Node 構成的同步隊列,是一個雙向隊列(不存在隊列實例,僅存在節點之間的關聯關系),將請求共享資源的線程封裝成 Node 節點來實現鎖的分配;同時利用內部類 ConditionObject 構建等待隊列,當調用 ConditionObject 的 await() 方法后,線程將會加入等待隊列中,當調用 ConditionObject 的 signal() 方法后,線程將從等待隊列轉移動同步隊列中進行鎖競爭。AQS 中只能存在一個同步隊列,但可擁有多個等待隊列。AQS 的 CLH 同步隊列的模型如下圖:
? ? ? ? AQS 有三個主要變量,分別是?head、tail、state,其中 head 指向同步隊列的頭部,注意 head 為空結點,不存儲信息。而 tail 則是同步隊列的隊尾,同步隊列采用的是雙向鏈表的結構是為了方便對隊列進行查找操作。當 Node 節點被設置為 head 后,其 thread 信息和前驅結點將被清空,因為該線程已獲取到同步狀態,正在執行了,也就沒有必要存儲相關信息了,head 只保存后繼結點的指針即可,便于 head 結點釋放同步狀態后喚醒后繼結點。
????????隊列的入隊和出隊操作都是無鎖操作,基于 CAS+自旋鎖 實現,AQS 維護了一個 volatile 修飾的 int 類型的 state 同步狀態,volatile 保證線程之間的可見性,并通過 CAS 對該同步狀態進行原子操作、實現對其值的修改。當 state=0 時,表示沒有任何線程占有共享資源的鎖,當 state=1 時,則說明當前有線程正在使用共享變量,其他線程必須加入同步隊列進行等待;
二、內部類 Node 數據結構分析:
static final class Node {//共享模式static final Node SHARED = new Node();//獨占模式static final Node EXCLUSIVE = null;//標識線程已處于結束狀態static final int CANCELLED = 1;//等待被喚醒狀態static final int SIGNAL = -1;//條件狀態static final int CONDITION = -2;//在共享模式中使用表示獲得的同步狀態會被傳播static final int PROPAGATE = -3;//等待狀態,存在CANCELLED、SIGNAL、CONDITION、PROPAGATE 4種取值volatile int waitStatus;//同步隊列中前驅結點volatile Node prev;//同步隊列中后繼結點volatile Node next;//請求鎖的線程volatile Thread thread;//等待隊列中的后繼結點,這個與Condition有關,稍后會分析Node nextWaiter;//判斷是否為共享模式final boolean isShared() {return nextWaiter == SHARED;}//..... }????????AQS分為兩種模式:獨占模式 EXCLUSIVE 和 共享模式 SHARED,像 ReentrantLock、CyclicBarrier 是基于獨占模式模式實現的,Semaphore,CountDownLatch 等是基于共享模式。
????????變量 waitStatus 表示當前封裝成 Node 節點的線程的等待狀態,共有4種取值 CANCELLED、SIGNAL、CONDITION、PROPAGATE:
-
CANCELLED:值為1,表示在同步隊列中的線程等待超時或者被中斷,處于已結束狀態,需要從同步隊列中移除該 Node 節點
-
SIGNAL:值為-1,表示后繼結點在等待當前結點喚醒。后繼結點入隊時,會將前繼結點的狀態更新為 SIGNAL,當該節點釋放了同步鎖之后,就會喚醒該節點的后繼節點
-
CONDITION:值為-2,與 Condition 相關,表示該結點在 condition 等待隊列中阻塞,當其他線程調用了Condition 的 signal() 方法后,CONDITION 狀態的結點將從等待隊列轉移到同步隊列中,等待獲取同步鎖。
-
PROPAGATE:值為-3時,在共享模式下使用,表示該線程以及后繼線程進行無條件傳播。前繼結點不僅會喚醒其后繼結點,同時也可能會喚醒后繼的后繼結點。
三、AQS 的設計模式:
3.1、AQS 的模板方法模式:
????????AQS 的基于模板方法模式設計的,在 AQS 抽象類中已經實現了線程在等待隊列的維護方式(如獲取資源失敗入隊/喚醒出隊等),而對于具體共享資源 state 的獲取與釋放(也就是鎖的獲取和釋放)則交由具體的同步器來實現,具體的同步器需要實現以下幾種方法:
-
isHeldExclusively():該線程是否正在獨占資源,只有用到 condition 才需要去實現它
-
tryAcquire(int):獨占模式,嘗試獲取資源,成功則返回 true,失敗則返回 false
-
tryRelease(int):獨占方式,嘗試釋放資源,成功則返回 true,失敗則返回 false
-
tryAcquireShared(int):共享方式,嘗試獲取資源。負數表示失敗;0表示成功,但沒有剩余可用資源;正數表示成功,且有剩余資源
-
tryReleaseShared(int):共享方式,嘗試釋放資源,如果釋放后允許喚醒后續等待結點返回true,否則返回false
3.2、JUC 中提供的同步器:
-
閉鎖 CountDownLatch:用于讓主線程等待一組事件全部發生后繼續執行。
-
柵欄 CyclicBarrier:用于等待其它線程,且會阻塞自己當前線程,所有線程必須全部到達柵欄位置后,才能繼續執行;且在所有線程到達柵欄處之后,可以觸發執行另外一個預先設置的線程。
-
信號量 Semaphore:用于控制訪問資源的線程個數,常常用于實現資源池,如數據庫連接池,線程池。在 Semaphore 中,acquire 方法用于獲取資源,有的話,繼續執行,沒有資源的話將阻塞直到有其它線程調用 release 方法釋放資源;
-
交換器 Exchanger:用于線程之間進行數據交換;當兩個線程都到達共同的同步點(都執行到exchanger.exchange 的時刻)時,發生數據交換,否則會等待直到其它線程到達;
CountDownLatch 和 CyclicBarrier 的區別?
兩者都可以用來表示代碼運行到某個點上,二者的區別在于:
① CyclicBarrier 的某個線程運行到某個位置之后就停止運行,直到所有的線程都到達了這個點,所有線程才重新運行;CountDownLatch 的某線程運行到某個位置之后,只是給計數值-1而已,該線程繼續運行;
② CyclicBarrier 可重用,CountDownLatch 不可重用,計數值 為 0 時該 CountDownLatch 就不可再用了。
推薦閱讀:https://juejin.cn/post/6989419875366076447
3.3、ReentranLock 中獨占模式下非公平鎖的獲取流程:
????????獲取獨占鎖的過程是定義在 tryAcquire() 中的,當前線程嘗試獲取同步狀態,如果獲取失敗,就將線程封裝成 Node 節點插入到 CLH 同步隊列中。插入同步隊列后,線程并沒有放棄獲取同步狀態,而是根據前置節點狀態狀態判斷是否繼續獲取,如果前置節點是 head 結點,繼續嘗試獲取,否則就將線程掛起。如果成功獲取同步狀態則將自己設置為 head 結點。當持有同步狀態的線程釋放資源后,也會喚醒隊列中的后繼線程。
四、ConditionObject 阻塞隊列:
?4.1、什么是 Condition 接口:
????????AQS 的阻塞隊列是基于內部類 ConditionObject 實現的,而 ConditionObject 實現了 Condition 接口。那 Condition 接口是什么呢?Condition 主要用于線程的等待和喚醒,在JDK5之前,線程的等待喚醒是用 Object 類的 wait/notify/notifyAll 方法實現的,這些方法必須配合 synchronized 關鍵字使用,使用起來不是很方便,為了解決這個問題,在 JDK5 之后,J.U.C 提供了Condition。
-
Condition.await 對應于 Object.wait;
-
Condition.signal 對應于 Object.notify;
-
Condition.signalAll 對應于 Object.notifyAll;
????????與 synchronized 的等待喚醒機制相比,Condition 能夠精細的控制多線程的休眠與喚醒,具備更多的靈活性, 通過多個 Condition 實例對象建立不同的等待隊列,從而實現同一個鎖擁有多個等待隊列。而 synchronized 關鍵字只能有一組等待喚醒隊列,使用 notify() 喚醒線程時只能隨機喚醒隊列中的一個線程。
4.2、ConditionObject 阻塞隊列實現原理:
????????Condition 的具體實現之一是 AQS 的內部類 ConditionObject,每個 Condition 都對應著一個等待隊列,也就是說如果一個鎖上創建了多個 Condition 對象,那么也就存在多個等待隊列。當調用 ConditionObject 的 await() 方法后,線程將會加入等待隊列中,當調用 ConditionObject 的 signal() 方法后,線程將從等待隊列轉移動同步隊列中進行鎖競爭。AQS 的 ConditionObject 中的等待隊列模型如下:
?4.3、AQS 的 線程喚醒機制原理:
?AQS 的線程喚醒是通過 singal() 方法實現的,我們先看下 singal() 方法線程喚醒的流程圖:
流程圖說明:
signal() 方法主要調用了 doSignal(),而 doSignal() 方法中做了兩件事:
- (1)從條件等待隊列移除被喚醒的節點,然后重新維護條件等待隊列的 firstWaiter 和 lastWaiter 的指向。
- (2)將從等待隊列移除的結點加入同步隊列(在 transferForSignal() 方法中完成的),如果進入到同步隊列失敗并且條件等待隊列還有不為空的節點,則繼續循環喚醒后續其他結點的線程
注意:無論是同步隊列還是等待隊列,使用的 Node 數據結構都是同一個,不過是使用的內部變量不同罷了
所以 signal() 的流程可以概述為:
-
signal() 被調用后,先判斷當前線程是否持有獨占鎖
-
如果有,那么喚醒當前 Condition 等待隊列的第一個結點的線程,并從等待隊列中移除該結點,添加到同步隊列中
-
如果加入同步隊列失敗,那么繼續循環喚醒等待隊列中的其他結點的線程
-
如果成功加入同步隊列,那么如果其前驅結點已結束或者設置前驅節點狀態為 Node.SIGNAL 狀態失敗,則通過 LockSupport.unpark() 喚醒被通知節點代表的線程
到此 signal() 任務完成,被喚醒后的線程,將調用 AQS 的 acquireQueued() 方法加入獲取同步狀態的競爭中,這就是等待喚醒機制的整個流程實現原理。
文章總結自:https://blog.csdn.net/javazejian/article/details/75043422
相關閱讀:https://juejin.cn/post/6844903997438951437
總結
以上是生活随笔為你收集整理的JUC多线程:AQS抽象队列同步器原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JUC多线程:JMM内存模型与volat
- 下一篇: JUC多线程:ThreadLocal 原