前言
Condition 是 Lock 的伴侶,至於如何使用,我們之前也寫了一些文章來說,例如 使用 ReentrantLock 和 Condition 實現一個阻塞佇列,併發程式設計之 Java 三把鎖, 在這兩篇文章中,我們都詳細介紹了他們的使用。今天我們就來深入看看原始碼實現。
構造方法
Condition
介面有 2 個實現類,一個是 AbstractQueuedSynchronizer.ConditionObject
,還有一個是 AbstractQueuedLongSynchronizer.ConditionObject
,都是 AQS 的內部類,該類結構如下:
幾個公開的方法:
- await()
- await(long time, TimeUnit unit)
- awaitNanos(long nanosTimeout)
- awaitUninterruptibly()
- awaitUntil(Date deadline)
- signal()
- signalAll()
今天我們重點關注 2 個方法,也是最常用的 2 個方法: await 和 signal。
await 方法
先貼一波程式碼加註釋:
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 建立一個新的節點,追加到 Condition 佇列中最後一個節點.
Node node = addConditionWaiter();
// 釋放這個鎖,並喚醒 AQS 佇列中一個執行緒.
int savedState = fullyRelease(node);
int interruptMode = 0;
// 判斷這個節點是否在 AQS 佇列上,第一次判斷總是返回 false
while (!isOnSyncQueue(node)) {
// 第一次總是 park 自己,開始阻塞等待
LockSupport.park(this);
// 執行緒判斷自己在等待過程中是否被中斷了,如果沒有中斷,則再次迴圈,會在 isOnSyncQueue 中判斷自己是否在佇列上.
// isOnSyncQueue 判斷當前 node 狀態,如果是 CONDITION 狀態,或者不在佇列上了(JDK 註釋說,由於 CAS 操作佇列上的節點可能會失敗),就繼續阻塞.
// isOnSyncQueue 判斷當前 node 還在佇列上且不是 CONDITION 狀態了,就結束迴圈和阻塞.
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
// 如果被中斷了,就跳出迴圈
break;
}
// 當這個執行緒醒來,會嘗試拿鎖, 當 acquireQueued 返回 false 就是拿到鎖了.
// interruptMode != THROW_IE >>> 表示這個執行緒沒有成功將 node 入隊,但 signal 執行了 enq 方法讓其入隊了.
// 將這個變數設定成 REINTERRUPT.
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 如果 node 的下一個等待者不是 null, 則進行清理,清理 Condition 佇列上的節點.
// 如果是 null ,就沒有什麼好清理的了.
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
// 如果執行緒被中斷了,需要丟擲異常.或者什麼都不做
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
複製程式碼
總結一下這個方法的邏輯:
- 在 Condition 中, 維護著一個佇列,每當執行 await 方法,都會根據當前執行緒建立一個節點,並新增到尾部.
- 然後釋放鎖,並喚醒阻塞在鎖的 AQS 佇列中的一個執行緒.
- 然後,將自己阻塞.
- 在被別的執行緒喚醒後, 將剛剛這個節點放到 AQS 佇列中.接下來就是那個節點的事情了,比如搶鎖.
- 緊接著就會嘗試搶鎖.接下來的邏輯就和普通的鎖一樣了,搶不到就阻塞,搶到了就繼續執行.
看看詳細的原始碼實現,Condition 是如何新增節點的?addConditionWaiter 方法如下:
// 該方法就是建立一個當前執行緒的節點,追加到最後一個節點中.
private Node addConditionWaiter() {
// 找到最後一個節點,放在區域性變數中,速度更快
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
// 如果最後一個節點失效了,就清除連結串列中所有失效節點,並重新賦值 t
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
// 建立一個當前執行緒的 node 節點
Node node = new Node(Thread.currentThread(), Node.CONDITION);
// 如果最後一個節點是 null
if (t == null)
// 將當前節點設定成第一個節點
firstWaiter = node;
else
// 如果不是 null, 將當前節點追加到最後一個節點
t.nextWaiter = node;
// 將當前節點設定成最後一個節點
lastWaiter = node;
// 返回
return node;
}
複製程式碼
建立一個當前執行緒的節點,追加到最後一個節點中. 當然,其中還有一個 unlinkCancelledWaiters 方法的呼叫,當最後一個節點失效了,就需要清理 Condition 佇列中無效的節點,程式碼如下:
// 清除連結串列中所有失效的節點.
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
// 當 next 正常的時候,需要儲存這個 next, 方便下次迴圈是連結到下一個節點上.
Node trail = null;
while (t != null) {
Node next = t.nextWaiter;
// 如果這個節點被取消了
if (t.waitStatus != Node.CONDITION) {
// 先將他的 next 節點設定為 null
t.nextWaiter = null;
// 如果這是第一次判斷 trail 變數
if (trail == null)
// 將 next 變數設定為 first, 也就是去除之前的 first(由於是第一次,肯定去除的是 first)
firstWaiter = next;
else
// 如果不是 null,說明上個節點正常,將上個節點的 next 設定為無效節點的 next, 讓 t 失效
trail.nextWaiter = next;
// 如果 next 是 null, 說明沒有節點了,那麼就可以將 trail 設定成最後一個節點
if (next == null)
lastWaiter = trail;
}
// 如果該節點正常,那麼就儲存這個節點,在下次連結下個節點時使用
else
trail = t;
// 換下一個節點繼續迴圈
t = next;
}
}
複製程式碼
那麼又是如何釋放鎖,並喚醒 AQS 佇列中的一個節點上的執行緒的呢?fullyRelease 方法如下:
final int fullyRelease(Node node) {
boolean failed = true;
try {
// 獲取 state 變數
int savedState = getState();
// 如果釋放成功,則返回 state 的大小,也就是之前持有鎖的執行緒的數量
if (release(savedState)) {
failed = false;
return savedState;
} else {
// 如果釋放失敗,丟擲異常
throw new IllegalMonitorStateException();
}
} finally {
//釋放失敗
if (failed)
// 將這個節點是指成取消狀態.隨後將從佇列中移除.
node.waitStatus = Node.CANCELLED;
}
}
複製程式碼
而 release 方法中,又是如何操作的呢?程式碼如下:
// 主要功能,就是釋放鎖,並喚醒阻塞在鎖上的執行緒.
public final boolean release(int arg) {
// 如果釋放鎖成功,返回 true, 可能會丟擲監視器異常,即當前執行緒不是持有鎖的執行緒.
// 也可能是釋放失敗,但 fullyRelease 基本能夠釋放成功.
if (tryRelease(arg)) {
// 釋放成功後, 喚醒 head 的下一個節點上的執行緒.
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
// 釋放失敗
return false;
}
複製程式碼
release 方法主要呼叫了 tryRelease 方法,該方法就是釋放鎖的。tryRelease 程式碼如下:
// 主要功能就是對 state 變數做減法, 如果 state 變成0,則將持有鎖的執行緒設定成 null.
protected final boolean tryRelease(int releases) {
// 計算 state
int c = getState() - releases;
// 如果當前執行緒不是持有該鎖的執行緒,則丟擲異常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 如果結果是 0,說明成功釋放了鎖.
if (c == 0) {
free = true;
// 將持有當前鎖的執行緒設定成 null.
setExclusiveOwnerThread(null);
}
// 設定變數
setState(c);
return free;
}
複製程式碼
好了,我們大概知道了 Condition 是如何釋放鎖的了,那麼又是如何將自己阻塞的呢?在將自己阻塞之前,需要呼叫 isOnSyncQueue 方法判斷,程式碼如下:
final boolean isOnSyncQueue(Node node) {
// 如果他的狀態不是等地啊,且他的上一個節點是 null, 便不在佇列中了
// 這裡判斷 == CONDITION,實際上是第一次判斷,而後面的判斷則是執行緒醒來後的判斷.
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
// 如果他的 next 不是 null, 說明他還在佇列上.
if (node.next != null) // If has successor, it must be on queue
return true;
// 如果從 tail 開始找上一個節點,找到了給定的節點,說明也在佇列上.返回 true.
return findNodeFromTail(node);
}
複製程式碼
實際上,第一次總是會返回 fasle,從而進入 while 塊呼叫 park 方法,阻塞自己,至此,Condition 成功的釋放了所在的 Lock 鎖,並將自己阻塞。
雖然阻塞了,但總有人會呼叫 signal 方法喚醒他,喚醒之後走下面的 if 邏輯,也就是 checkInterruptWhileWaiting 方法,看名字是當等待的時候檢查中斷狀態,程式碼如下:
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ?
// transferAfterCancelledWait >>>> 如果將 node 放入 AQS 佇列失敗,就返回 REINTERRUPT, 成功則返回 THROW_IE
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) : 0;
}
複製程式碼
當執行緒中斷了,就需要根據呼叫 transferAfterCancelledWait 方法的返回值來返回不同的常量,該方法內部邏輯是怎麼樣的呢?
final boolean transferAfterCancelledWait(Node node) {
// 將 node 的狀態設定成 0 成功後,將這個 node 放進 AQS 佇列中.
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
enq(node);
return true;
}
// 如果 CAS 失敗, 返回 false ,
// 當 node 不在 AQS 節點上, 就自旋. 直到 enq 方法完成.
// JDK 認為, 在這個過程中, node 不在 AQS 佇列上是少見的,也是暫時的.所以自旋.
// 如果不自旋的話,後面的邏輯是無法繼續執行的. 實際上,自旋是在等待在 signal 中執行 enq 方法讓 node 入隊.
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}
複製程式碼
其實是嘗試將自己放入到佇列中。如果無法放入,就自旋等待 signal 方法放入。
回到 await 方法,繼續向下走,執行 3 個 if 塊,第一個 if 塊,嘗試拿鎖,為什麼?因為這個時候,這個執行緒已經被喚醒了,而且他在 AQS 的佇列中,那麼,他就需要在醒的時候,去拿鎖。acquireQueued 方法是拿鎖的邏輯,程式碼如下:
// 返回結果:是否被中斷了, 當返回 false 就是拿到鎖了,反之沒有拿到.
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 返回他的上一個節點
final Node p = node.predecessor();
// 如果這個節點的上個節點是 head, 且成功獲取了鎖.
if (p == head && tryAcquire(arg)) {
// 將當前節點設定成 head
setHead(node);
// 他的上一個節點(head)設定成 null.
p.next = null; // help GC
failed = false;
// 返回 false,沒有中斷
return interrupted;
}
// shouldParkAfterFailedAcquire >>> 如果沒有獲取到鎖,就嘗試阻塞自己等待(上個節點的狀態是 -1 SIGNAL).
// parkAndCheckInterrupt >>>> 返回自己是否被中斷了.
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
複製程式碼
注意,如果這裡拿不到鎖,就會在 parkAndCheckInterrupt 方法中阻塞。這裡和正常的 AQS 中的佇列節點是一摸一樣的,沒有特殊。
這裡有個 tryAcquire 方法需要注意一下: 這個就是嘗試拿鎖的邏輯所在。程式碼如下:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 如果鎖的狀態是空閒的.
if (c == 0) {
// !hasQueuedPredecessors() >>> 是否含有比自己的等待的時間長的執行緒, false >> 沒有
// compareAndSetState >>> CAS 設定 state 變數成功
// 設定當前執行緒為鎖的持有執行緒成功
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
// 上面 3 個條件都滿足, 搶鎖成功.
return true;
}
}
// 如果 state 狀態不是0, 且當前執行緒和鎖的持有執行緒相同,則認為是重入.
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
複製程式碼
主要邏輯是設定 state 變數,將鎖的持有執行緒變成自己。這些是在沒有比自己等待時間長的執行緒的情況下發生的。意思是,優先哪些等待時間久的執行緒拿鎖。當然,這裡還有一些重入的邏輯。
後面的兩個 if 塊就簡單了,如果 Condition 中還有節點,那麼就嘗試清理無效的節點,呼叫的是 unlinkCancelledWaiters 方法,這個方法,我們在上面分析過了,就不再重複分析了。
最後,判斷是否中斷,執行 reportInterruptAfterWait 方法,這個方法可能會丟擲異常,也可能會對當前執行緒打一箇中斷標記。
程式碼如下:
private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
if (interruptMode == THROW_IE)
throw new InterruptedException();
else if (interruptMode == REINTERRUPT)
selfInterrupt();
}
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
複製程式碼
好,關於 await 方法就分析完了,可以看到,樓主貼了很多的註釋,事實上,都是為了以後能更好的複習,也方便感興趣的同學在看原始碼的時候結合我的註釋一起分析。
這個方法的總結都在開始說了,就不再重複總結了。後面,我們會結合 signal 方法一起總結。
再來看看 signal 方法實現。
signal 方法
程式碼如下:
public final void signal() {
// 如果當前執行緒不是持有該鎖的執行緒.丟擲異常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 拿到 Condition 佇列上第一個節點
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
複製程式碼
很明顯,喚醒策略是從頭部開始的。
看看 doSignal(first) 方法實現:
private void doSignal(Node first) {
do {
// 如果第一個節點的下一個節點是 null, 那麼, 最後一個節點也是 null.
if ((firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
// 將 next 節點設定成 null.
first.nextWaiter = null;
// 如果修改這個 node 狀態為0失敗了(也就是喚醒失敗), 並且 firstWaiter 不是 null, 就重新迴圈.
// 通過從 First 向後找節點,直到喚醒或者沒有節點為止.
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
複製程式碼
重點在於 transferForSignal 方法,該方法肯定做了喚醒操作。
final boolean transferForSignal(Node node) {
// 如果不能改變狀態,就取消這個 node.
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 將這個 node 放進 AQS 的佇列,然後返回他的上一個節點.
Node p = enq(node);
int ws = p.waitStatus;
// 如果上一個節點的狀態被取消了, 或者嘗試設定上一個節點的狀態為 SIGNAL 失敗了(SIGNAL 表示: 他的 next 節點需要停止阻塞),
// 喚醒輸入節點上的執行緒.
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
// 如果成功修改了 node 的狀態成0,就返回 true.
return true;
}
複製程式碼
果然,看到了 unpark 操作,該方法先是 CAS 修改了節點狀態,如果成功,就將這個節點放到 AQS 佇列中,然後喚醒這個節點上的執行緒。此時,那個節點就會在 await 方法中甦醒,並在執行 checkInterruptWhileWaiting 方法後開始嘗試獲取鎖。
總結
總結一下 Condition 執行 await 和 signal 的過程吧。
-
首先,執行緒如果想執行 await 方法,必須拿到鎖,在 AQS 裡面,搶到鎖的一般都是 head,然後 head 失效,從佇列中刪除。
-
在當前執行緒(也就是 AQS 的 head)拿到鎖後,呼叫了 await 方法,第一步建立一個 Node 節點,放到 Condition 自己的佇列尾部,並喚醒 AQS 佇列中的某個(head)節點,然後阻塞自己,等待被 signal 喚醒。
-
當有其他執行緒呼叫了 signal 方法,就會喚醒 Condition 佇列中的 first 節點,然後將這個節點放進 AQS 佇列的尾部。
-
阻塞在 await 方法的執行緒甦醒後,他已經從 Condition 佇列總轉移到 AQS 佇列中了,這個時候,他就是一個正常的 AQS 節點,就會嘗試搶鎖。並清除 Condition 佇列中無效的節點。
下面這張圖說明了這幾個步驟。
好啦,關於 Condition 就說到這裡啦,總的來說,就是在 Condition 中新增一個休眠佇列來實現的。只要呼叫 await 方法,就會休眠,進入 Condition 佇列,呼叫 signal 方法,就會從 Condition 佇列中取出一個執行緒並插入到 AQS 佇列中,然後喚醒,讓這個執行緒自己去搶鎖。