Condition
的使用接觸最多是在 ReentrantLock
,講 AQS
到這裏應該就完結了,前兩篇文章理了下 AQS
的共享鎖模式和獨立鎖模式,這篇在理下其中 Condition
的原理。
我在第一篇 手撕AQS之共享鎖 有提到同步操作的本質,大概遵循以下流程:
- 持有鎖;
- 操作共享資源;
- 釋放鎖;
無論是使用共享或者獨佔鎖,都無法在持有鎖的中途將鎖讓出,並繼續等待着獲取鎖。這就像使用 synchronized
同步對象一樣,但 synchronized
可以利用被同步對象的 wait
方法讓出鎖,當該對象的 notify
被調用時,它又能重新競爭鎖。所以,使用 ReentrantLock
怎樣實現這個呢?AQS
中實現了 Condition
接口的 ConditionObject
類能夠幫助我們做到。
整體思想:ConditionObject
維護了條件隊列,當調用 await 時,將創建 node 並加入該條件隊列,當 signal 時,node 會從條件隊列取出,加入 CLH 隊列(這在前兩篇文章中有講),這和之前的邏輯並無兩樣。那現在,我們來看看關鍵的 await 和 signal 。
await:
public final void await() throws InterruptedException {
// 首先檢查線程是否中斷
if (Thread.interrupted())
throw new InterruptedException();
// 創建 node 節點,添加到隊尾
Node node = addConditionWaiter();
// 1.釋放狀態值,並保存曾持有的狀態值
int savedState = fullyRelease(node);
int interruptMode = 0;
// 2.該節點不處於非同步隊列,暫停線程
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 3.請求入CLH 隊列
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
-
釋放狀態值,並保存曾持有的狀態值,目的是爲了在重新獲得鎖後,恢復曾持有的狀態值:
final int fullyRelease(Node node) { boolean failed = true; try { int savedState = getState(); // 真正的釋放鎖,會在方法內調用子類的實現 if (release(savedState)) { failed = false; return savedState; } else { // 這就是爲什麼調用 await 方法必須要獲取到鎖的原因 throw new IllegalMonitorStateException(); } } finally { // 失敗的話,通過該狀態值,該節點能夠從隊列中移除 if (failed) node.waitStatus = Node.CANCELLED; } }
-
該節點不處於同步隊列(
CLH
隊列),暫停線程:final boolean isOnSyncQueue(Node node) { // 還是屬於條件隊列(prev可以是非空的,但不足以判斷其在隊列上,因爲把它放在隊列上的CAS可能會失敗。) if (node.waitStatus == Node.CONDITION || node.prev == null) return false; // 進入同步隊列,會爲該成員賦值,條件隊列是通過 `nextWaiter` 成員維護的 if (node.next != null) // If has successor, it must be on queue return true; // 能到這裏,這個節點基本在隊列尾部 return findNodeFromTail(node); } // 從同步隊列循環查找該節點,如果存在,則返回 true private boolean findNodeFromTail(Node node) { Node t = tail; for (;;) { if (t == node) return true; if (t == null) return false; t = t.prev; } }
-
請求入CLH 隊列,這就和 手撕AQS之獨佔鎖模式 中的一樣了。
signal:
public final void signal() {
// 調用該方法的線程必須持有鎖
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 通知等待最長的節點
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
private void doSignal(Node first) {
do {
// 從隊列中移除該節點,並將它的下一節點放置隊頭
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
transferForSignal
方法能夠轉變該節點的狀態,併入同步隊列:
final boolean transferForSignal(Node node) {
/*
* If cannot change waitStatus, the node has been cancelled.
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* Splice onto queue and try to set waitStatus of predecessor to
* indicate that thread is (probably) waiting. If cancelled or
* attempt to set waitStatus fails, wake up to resync (in which
* case the waitStatus can be transiently and harmlessly wrong).
*/
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
文檔說的很清楚,我就保留了。需要解釋的是爲什麼前繼節點的 waitStatus 爲取消或者將前繼節點的 waitStatus 設置爲 SIGNAL 失敗,需要喚醒當前線程?成功的話,就將前繼節點更新爲 SIGNAL ,這是需要的,失敗的話,會重新執行下面這個 await 方法中的循環:
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
final boolean transferAfterCancelledWait(Node node) {
// 上面這種情況這裏會失敗掉。
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
enq(node);
return true;
}
/*
* If we lost out to a signal(), then we can't proceed
* until it finishes its enq(). Cancelling during an
* incomplete transfer is both rare and transient, so just
* spin.
*/
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}
在最後帶來的影響也就是它跳出循環,進入競爭鎖的階段,在這個階段,還是有可能被阻塞。
只要進入同步隊列,就有機會被喚醒,然後在 await
方法中能夠結束:
// 3.請求入CLH 隊列
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
如果是在同步隊列中喚醒該線程(與上面介紹的錯誤流程到這裏是不同的),也會進入到這裏,嘗試再次請求(acquireQueued
),成功的話,就可以結束了,之前的線程就能恢復執行了。
總結:
這裏比較繞的一個點就是線程恢復執行的問題。舉個列子,現在有線程A 調用 await 方法阻塞了,那麼它 park 的位置在 await 方法裏,如果線程 A 調用 lock 方法阻塞了,那麼它最後的 park 位置在 acquireQueued
方法裏調用的 parkAndCheckInterrupt
方法。而 await 被喚醒,會從 await 中結束掉,進入 acquireQueued
方法, 如果最後沒有獲取到鎖,仍然會進入 parkAndCheckInterrupt
方法裏 park。這是比較重要的一個點,需要理解清楚!
我與風來
認認真真學習,做思想的產出者,而不是文字的搬運工
錯誤之處,還望指出