帶著問題閱讀
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(int); // 嘗試獲取獨佔鎖,可獲取返回true,否則false
tryRelease(int); // 嘗試釋放獨佔鎖,可釋放返回true,否則false
tryAcquireShared(int); // 嘗試以共享方式獲取鎖,失敗返回負數,只能獲取一次返回0,否則返回個數
tryReleaseShared(int); // 嘗試釋放共享鎖,可獲取返回true,否則false
isHeldExclusively(); // 判斷執行緒是否獨佔資源
如實現類只需實現獨佔鎖/共享鎖功能,可只實現tryAcquire/tryRelease
或tryAcquireShared/tryReleaseShared
。雖然實現tryAcquire/tryRelease
可自行設定邏輯,但建議使用state
方法對state
變數進行操作以實現同步類。
如下是一個簡單的同步鎖實現示例:
public class Mutex extends AbstractQueuedSynchronizer {
@Override
public boolean tryAcquire(int arg) {
return compareAndSetState(0, 1);
}
@Override
public 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
public final void acquire(int arg) {
// tryAcquire需實現類處理
// 如獲取資源成功,直接返回
if (!tryAcquire(arg) &&
// 如獲取資源失敗,將執行緒包裝為Node新增到佇列中阻塞等待
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 如阻塞執行緒被打斷
selfInterrupt();
}
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
public final void acquireShared(int arg) {
// 負數表示獲取共享鎖失敗,不同於tryAcquire的bool返回
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
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
的核心定義為:
public class ConditionObject implements Condition, java.io.Serializable {
...
private transient Node firstWaiter;
private transient Node lastWaiter;
...
}
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
大同小異,不再贅述。