15 張圖帶你徹底理解 java AQS
java 中 AQS 是 AbstractQueuedSynchronizer 類,AQS 依賴 FIFO 隊列來提供一個框架,這個框架用於實現鎖以及鎖相關的同步器,比如信號量、事件等。
在 AQS 中,主要有兩部分功能,一部分是操作 state 變量,第二部分是實現排隊和阻塞機制。
注意,AQS 並沒有實現任何同步接口,它只是提供了類似 acquireInterruptible 的方法,調用這些方法可以實現鎖和同步器。
管程模型
java 使用 MESA 管程模型來管理類的成員變量和方法,讓這個類的成員變量和方法的操作是線程安全的。下圖是 MESA 管程模型,裏面除了定義共享變量外,還定義了條件變量和條件變量等待隊列:
java 中的 MESA 模型有一點改進,就是管程內部只有一個條件變量和一個等待隊列。下圖是 AQS 的管程模型:
AQS 的管程模型依賴 AQS 中的 FIFO 隊列實現入口等待隊列,而 ConditionObject 則實現了條件隊列,這個隊列可以創建多個。本文主要講解入口等待隊列獲取鎖的幾種方式。參考 1[1]
獲取獨佔鎖
獨佔, 忽略 interrupts
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
這裏的 tryAcquire 是抽象方法,有 AQS 的子類來實現,因爲每個子類實現的鎖是不一樣的。
入隊
上面的代碼可以看到,獲取鎖失敗後,會先執行 addWaiter 方法加入隊列,然後執行 acquireQueued 方法自旋地獲取鎖直到成功。
addWaiter 代碼邏輯如下圖,簡單說就是把 node 入隊,入隊後返回 node 參數給 acquireQueued 方法:
這裏有一個點需要注意,如果隊列爲空,則新建一個 Node 作爲隊頭。
入隊後獲取鎖
acquireQueued 自旋獲取鎖邏輯如下圖:
這裏有幾個細節:
1.waitStatus
-
CANCELLED(1):當前節點取消獲取鎖。當等待超時或被中斷 (響應中斷),會觸發變更爲此狀態,進入該狀態後節點狀態不再變化。
-
SIGNAL(-1):後面節點等待當前節點喚醒。
-
CONDITION(-2):Condition 中使用,當前線程阻塞在 Condition,如果其他線程調用了 Condition 的 signal 方法,這個結點將從等待隊列轉移到同步隊列隊尾,等待獲取同步鎖。
-
PROPAGATE(-3):共享模式,前置節點喚醒後面節點後,喚醒操作無條件傳播下去。
-
0:中間狀態,當前節點後面的節點已經喚醒,但是當前節點線程還沒有執行完成。
- 獲取鎖失敗後掛起
如果前置節點不是頭節點,或者前置節點是頭節點但當前節點獲取鎖失敗,這時當前節點需要掛起,分三種情況,
前置節點 waitStatus=-1,如下圖:
前置節點 waitStatus > 0,如下圖:
前置節點 waitStatus < 0 但不等於 -1,如下圖:
- 取消獲取鎖
如果獲取鎖拋出異常,則取消獲取鎖,如果當前節點是 tail 節點,分兩種情況如下圖:
如果當前節點不是 tail 節點,也分兩種情況,如下圖:
-
對中斷狀態忽略
-
如果前置節點的狀態是 0 或 PROPAGATE,會被當前節點自旋過程中更新成 - 1,以便之後通知當前節點。
獨佔 + 響應中斷
對應方法 acquireInterruptibly(int arg)。
跟忽略中斷 (acquire 方法) 不同的是要響應中斷,下面兩個地方響應中斷:
-
獲取鎖之前會檢查當前線程是否中斷。
-
獲取鎖失敗入隊,在隊列中自旋獲取鎖的過程中也會檢查當前線程是否中斷。
如果檢查到當前線程已經中斷,則拋出 InterruptedException,當前線程退出。
獨佔 + 響應中斷 + 考慮超時
對應方法 tryAcquireNanos(int arg, long nanosTimeout)。
這個方法具備了獨佔 + 響應中斷 + 超時的功能,下面 2 個地方要判斷是否超時:
-
自旋獲取鎖的過程中每次獲取鎖失敗都要判斷是否超時
-
獲取鎖失敗 park 之前要判斷超時時間是否大於自旋的閾值時間 (spinForTimeoutThreshold = 1ns)
另外,park 線程的操作使用 parkNanos 傳入阻塞時間。
釋放獨佔鎖
獨佔鎖釋放分兩步:釋放鎖,喚醒後繼節點。
釋放鎖的方法 tryRelease 是抽象的,由子類去實現。
我們看一下喚醒後繼節點的邏輯,首先需要滿足兩個條件:
-
head 節點不等於 null
-
head 節點 waitStatus 不等於 0
這裏有兩種情況 (在方法 unparkSuccessor):
- 情況一,後繼節點 waitStatus <= 0,直接喚醒後繼節點,如下圖:
- 情況二:後繼節點爲空或者 waitStatus > 0,從後往前查找最接近當前節點的節點進行喚醒,如下圖:
獲取共享鎖
之前我們講了獨佔鎖,這一小節我們談共享鎖,有什麼不同呢?
共享,忽略 interrupts
對應方法 acquireShared,代碼如下:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
tryAcquireShared
這裏獲取鎖使用的方法是 tryAcquireShared,獲取的是共享鎖。獲取共享鎖跟獲取獨佔鎖不同的是,會返回一個整數值,說明如下:
-
返回負數:獲取鎖失敗。
-
返回 0:獲取鎖成功但是之後再由線程來獲取共享鎖時就會失敗。
-
返回正數: 獲取鎖成功而且之後再有線程來獲取共享鎖時也可能會成功。所以需要把喚醒操作傳播下去。
tryAcquireShared 獲取鎖失敗後 (返回負數),就需要入隊後自旋獲取,也就是執行方法 doAcquireShared。
doAcquireShared
怎麼判斷隊列中等待節點是在等待共享鎖呢?nextWaiter == SHARED,這個參數值是入隊新建節點的時候構造函數傳入的。
自旋過程中,如果獲取鎖成功 (返回正數),首先把自己設置成新的 head 節點,然後把通知傳播下去。如下圖:net/anlian523/article/details/106319294/
之後會喚醒後面節點並保證喚醒操作可以傳播下去。但是需要滿足四個條件中的一個:
-
tryAcquireShared 返回值大於 0,有多餘的鎖,可以繼續喚醒後繼節點
-
舊的 head 節點 waitStatus < 0,應該是其他線程釋放共享鎖過程中把它的狀態更新成了 - 3
-
新的 hade 節點 waitStatus < 0,只要不是 tail 節點,就可能是 - 1
這裏會造成不必要的喚醒,因爲喚醒後獲取不到鎖只能繼續入隊等待
- 當前節點的後繼節點是空或者非空但正在等待共享鎖
喚醒後面節點的操作,其實就是釋放共享鎖,對應方法是 doReleaseShared,見釋放共享鎖一節。
共享 + 響應中斷
對應方法 acquireSharedInterruptibly(int arg)。
跟共享忽略中斷 (acquireShared 方法) 不同的是要響應中斷,下面兩個地方響應中斷:
-
獲取鎖之前會檢查當前線程是否中斷。
-
獲取鎖失敗入隊,在隊列中自旋獲取鎖的過程中也會檢查當前線程是否中斷。
如果檢查到當前線程已經中斷,則拋出 InterruptedException,當前線程退出。
共享 + 響應中斷 + 考慮超時
對應方法 tryAcquireSharedNanos(int arg, long nanosTimeout)。
這個方法具備了共享 + 響應中斷 + 超時的功能,下面 2 個地方要判斷是否超時:
-
自旋獲取鎖的過程中每次獲取鎖失敗都要判斷是否超時
-
獲取鎖失敗 park 之前要判斷超時時間是否大於自旋的閾值時間 (spinForTimeoutThreshold = 1ns)
另外,park 線程的操作使用 parkNanos 傳入阻塞時間。
釋放共享鎖
釋放共享鎖代碼如下:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
首先嚐試釋放共享鎖,tryReleaseShared 代碼由子類來實現。釋放成功後執行 AQS 中的 doReleaseShared 方法,是一個自旋操作。
自旋的條件是隊列中至少有兩個節點,這裏分三種情況。
情況一:當前節點 waitStatus 是 - 1,如下圖:
情況二:當前節點 waitStatus 是 0(被其他線程更 xin 新成了中間狀態),如下圖:
情況三:當前節點 waitStatus 是 - 3,爲什麼會這樣呢?**需要解釋一下,head 節點喚醒後繼節點之前 waitStatus 已經被更新中間態 0 了,喚醒後繼節點動作還沒有執行,又被其他線程更成了 - 3,也就是其他線程釋放鎖執行了上面情況二。**這時需要先把 waitStatus 再更成 0(在方法 unparkSuccessor),如下圖:
抽象方法
上面的講解可以看出,如果要基於 AQS 來實現併發鎖,可以根據需求重寫下面四個方法來實現,這四個方法在 AQS 中沒有具體實現:
-
tryAcquire(int arg):獲取獨佔鎖
-
tryRelease(int arg):釋放獨佔鎖
-
tryAcquireShared(int arg):獲取共享鎖
-
tryReleaseShared(int arg):釋放共享鎖
參考 2[2]
AQS 的子類需要重寫上面的方法來修改 state 值,並且定義獲取鎖或者釋放鎖時 state 值的變化。子類也可以定義自己的 state 變量,但是隻有更新 AQS 中的 state 變量纔會對同步起作用。
還有一個判斷當前線程是否持有獨佔鎖的方法 isHeldExclusively,也可以供子類重寫後使用。
獲取 / 釋放鎖的具體實現放到下篇文章講解。
總結
AQS 使用 FIFO 隊列實現了一個鎖相關的併發器模板,可以基於這個模板來實現各種鎖,包括獨佔鎖、共享鎖、信號量等。
AQS 中,有一個核心狀態是 waitStatus,這個代表節點的狀態,決定了當前節點的後續操作,比如是否等待喚醒,是否要喚醒後繼節點。
參考資料
[1]
參考 1: https://blog.csdn.net/it_lihongmin/article/details/109609023
[2]
參考 2: https://mp.weixin.qq.com/s/u_0fAgTUtC0YUz-SIxlL7Q
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/CcCC22B7CcjdApk3dpRfEA