你真的弄明白了吗?Java并发之AQS详解
你真的弄明白了嗎?Java并發之AQS詳解
帶著問題閱讀
1、什么是AQS,它有什么作用,核心思想是什么
2、AQS中的獨占鎖和共享鎖原理是什么,AQS提供的鎖機制是公平鎖還是非公平鎖
3、AQS在Java中有哪些實現,如何基于AQS實現自己的鎖控制
4、AQS除了提供鎖框架以外還提供了什么能力
AQS介紹
AbstractQueuedSynchronizer(AQS) 提供了一套可用于實現鎖同步機制的框架,不夸張地說, AQS 是 JUC 同步框架的基石。 AQS 通過一個 FIFO 隊列維護線程同步狀態,實現類只需要繼承該類,并重寫指定方法即可實現一套線程同步機制。
AQS 根據資源互斥級別提供了 獨占和共享 兩種資源訪問模式;同時其定義 Condition 結構提供了 wait/signal 等待喚醒機制。在 JUC 中,諸如 ReentrantLock 、 CountDownLatch 等都基于 AQS 實現。
AQS框架
AQS原理
AQS 的原理并不復雜, AQS 維護了一個 volatile int state 變量和一個 CLH(三個人名縮寫)雙向隊列 ,隊列中的節點持有線程引用,每個節點均可通過 getState() 、 setState() 和 compareAndSetState() 對 state 進行修改和訪問。·
當線程獲取鎖時,即試圖對 state 變量做修改,如修改成功則獲取鎖;如修改失敗則包裝為節點掛載到隊列中,等待持有鎖的線程釋放鎖并喚醒隊列中的節點。
AQS模版方法
AQS 內部封裝了隊列維護邏輯,采用模版方法的模式提供實現類以下方法:
如實現類只需實現獨占鎖/共享鎖功能,可只實現 tryAcquire/tryRelease 或 tryAcquireShared/tryReleaseShared 。雖然實現 tryAcquire/tryRelease 可自行設定邏輯,但建議使用 state 方法對 state 變量進行操作以實現同步類。
如下是一個簡單的同步鎖實現示例:
public class Mutex extends AbstractQueuedSynchronizer {@Overridepublic boolean tryAcquire(int arg) {return compareAndSetState(0, 1);}@Overridepublic boolean tryRelease(int arg) {return compareAndSetState(1, 0);}public static void main(String[] args) {final Mutex mutex = new Mutex();new Thread(() -> {System.out.println("thread1 acquire mutex");mutex.acquire(1);// 獲取資源后sleep保持try {TimeUnit.SECONDS.sleep(5);} catch(InterruptedException ignore) {}mutex.release(1);System.out.println("thread1 release mutex");}).start();new Thread(() -> {// 保證線程2在線程1啟動后執行try {TimeUnit.SECONDS.sleep(1);} catch(InterruptedException ignore) {}// 等待線程1 sleep結束釋放資源mutex.acquire(1);System.out.println("thread2 acquire mutex");mutex.release(1);}).start()} }示例代碼簡單通過 AQS 實現一個互斥操作,線程1獲取 mutex 后,線程2的 acquire 陷入阻塞,直到線程1釋放。其中 tryAcquire/acquire/tryRelease/release 的 arg 參數可按實現邏輯自定義傳入值,無具體要求。
@param arg the acquire argument. This value is conveyed to {@link #tryAcquire} but is otherwise uninterpreted and can represent anyting you like.AQS核心結構
Node
前文提到,在 AQS 中如果線程獲取資源失敗,會包裝成一個節點掛載到 CLH 隊列上, AQS 中定義了 Node 類用于包裝線程。
Node 主要包含5個核心字段:
waitStatus :當前節點狀態,該字段共有5種取值:
CANCELLED = 1 。節點引用線程由于等待超時或被打斷時的狀態。
SIGNAL = -1 。后繼節點線程需要被喚醒時的當前節點狀態。當隊列中加入后繼節點被掛起 (block) 時,其前驅節點會被設置為 SIGNAL 狀態,表示該節點需要被喚醒。
CONDITION = -2 。當節點線程進入 condition 隊列時的狀態。(見 ConditionObject )
PROPAGATE = -3 。僅在釋放共享鎖 releaseShared 時對頭節點使用。(見共享鎖分析)
0 。節點初始化時的狀態。
prev :前驅節點。
next :后繼節點。
thread :引用線程,頭節點不包含線程。
nextWaiter : condition 條件隊列。(見 ConditionObject )
獨占鎖分析
acquire
acquire 核心為 tryAcquire 、 addWaiter 和 acquireQueued 三個函數,其中 tryAcquire 需具體類實現。 每當線程調用 acquire 時都首先會調用 tryAcquire ,失敗后才會掛載到隊列,因此 acquire 實現 默認為非公平鎖 。
addWaiter 將線程包裝為獨占節點,尾插式加入到隊列中,如隊列為空,則會添加一個空的頭節點。值得注意的是 addWaiter 中的 enq 方法,通過 CAS+自旋 的方式處理尾節點添加沖突。
acquireQueue 在線程節點加入隊列后判斷是否可再次嘗試獲取資源,如不能獲取則將其前驅節點標志為 SIGNAL 狀態(表示其需要被 unpark 喚醒)后,則通過 park 進入阻塞狀態。
參照流程圖, acquireQueued 方法核心邏輯為 for(;😉 和 shouldParkAfterFailedAcquire 。 tail 節點默認初始狀態為0,當新節點被掛載到隊列后,將其前驅即原 tail 節點狀態設為 SIGNAL ,表示該節點需要被喚醒,返回 true 后即被 park 陷入阻塞。 for 循環直到節點前驅為 head 后才嘗試進行資源獲取。
release
release 流程較為簡單,嘗試釋放成功后,即從頭結點開始喚醒其后繼節點,如后繼節點被取消,則轉為從尾部開始找阻塞的節點將其喚醒。阻塞節點被喚醒后,即進入 acquireQueued 中的 for(;😉 循環開始新一輪的資源競爭。
共享鎖分析
acquireShared & releaseShared
acquireShared 和 releaseShared 整體流程與獨占鎖類似, tryAcquireShared 獲取失敗后以 Node.SHARED 掛載到隊尾阻塞,直到隊頭節點將其喚醒。在 doAcquireShared 與獨占鎖不同的是,由于共享鎖是可以被多個線程獲取的,因此在首個阻塞節點被喚醒后,會通過 setHeadAndPropagate 傳遞喚醒后續的阻塞節點。
// doAcquireShared核心代碼 final Node node = addWaiter(Node.SHARED); ... for (;;) {final Node p = node.predecessor();if (p == head) {int r = tryAcquireShared(arg);if (r >= 0) {// r>=0 表示獲取鎖成功,調整頭結點并傳遞喚醒setHeadAndPropagate(node, r);}}... }setHeadAndPropagate 和 doReleaseShared 構成共享鎖喚醒的核心邏輯。
這兩方法的邏輯較為簡單,不再進行展開,主要對 setheadAndPropagate 的多節點喚醒判斷邏輯做出分析。
進入 setHeadAndPropagate ,首先需要明確的是,該函數的傳入參數 propagate 一定是非負數,接下來其喚醒主要為兩個判斷邏輯:
如果 propagate > 0 ,表示存在多個共享鎖可以獲取,可直接進行 doReleaseShared 喚醒阻塞節點。
如果 propagate = 0 ,表示僅當前節點可被喚醒,則有兩種情況:
h == null || h.waitStatus < 0 ,通常情況下 h != null ,現給出 h.waitStatus < 0 的場景。
(h = head) == null || h.waitStatus < 0 的場景執行序列如下:
獨占鎖共享鎖小結
1、獨占鎖共享鎖默認都是非公平獲取策略,可能被插隊。
2、獨占鎖只有一個線程可獲取,其他線程均被阻塞在隊列中;共享鎖可以有多個線程獲取。
3、獨占鎖釋放僅喚醒一個阻塞節點,共享鎖可以根據可用數量,一次喚醒多個阻塞節點
ConditionObject
AQS 中 Node 除了組成阻塞隊列外,還在 ConditionObject 中得到應用, ConditionObject 的核心定義為:
ConditionObject 通過 Node 也構成了一個 FIFO 的隊列,那么 ConditionObject 為 AQS 提供了怎樣的功能呢?
public interface Condition {...void await() throws InterruptedException;void signal();void signalAll();... }查看 Condition 接口的定義,可以看到其定義的方法與 Object 類的 wait/notify/notifyAll 功能是一致的。
在Synchronized詳解中筆者曾對 ObjectMonitor 做過簡單介紹,其中 ObjectMonitor 包含 _WaitSet 和 _EntryList 兩個隊列,分別用于存儲 wait調用 和 sychronized鎖競爭 時掛起的線程,而 AQS 通過 ConditionObject 同樣也提供了 wait/notify 機制的阻塞隊列。
Condihttps://blog.csdn.net/anlian523/article/details/106319294/tionObject 機制如上圖,在條件隊列中, Node 采用 nextWaiter 組成單向鏈表,當持有鎖的線程發起 condition.await 調用后,會包裝為 Node 掛載到 Condition條件阻塞隊列中 ;當對應 condition.signal 被觸發后,條件阻塞隊列中的節點將被喚醒并掛載到 鎖阻塞隊列 中。 ConditionObject 的隊列邏輯與前述的 acquire/release 大同小異,不再贅述。
多多支持Remi醬吧!
愛你哦~
文章來源:https://www.tuicool.com/articles/iYzUfa2
總結
以上是生活随笔為你收集整理的你真的弄明白了吗?Java并发之AQS详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java的多线程和线程池的使用,你真的清
- 下一篇: 如何用 Redis 实现延迟队列?