Condition接口
這個接口爲我們提供了2類方法,await()和signal(),其實現類ConditionObject,是AQS中的一個子類。在介紹AQS結構的文章中,ConditionObject類被跳過了,這個類的存在與CLH模型關聯度不是很強,但在併發編程中卻是不可或缺的一環,它提供的await()和signal()方法,能夠爲多線程之間交互提供幫助,能讓線程暫停和恢復,是很重要的方法。
ConditionObjec類
我們先來看一下它的內部結構。
成員變量:看來ConditionObject中也維護着一個隊列,我們稱它爲“等待隊列”。
private transient Node firstWaiter; // 首節點
private transient Node lastWaiter; // 尾結點
常量:
/** Mode meaning to reinterrupt on exit from wait */
private static final int REINTERRUPT = 1; // 從等待狀態切換爲中斷狀態
/** Mode meaning to throw InterruptedException on exit from wait */
private static final int THROW_IE = -1; // 拋出異常標識
實例方法很多,我們從最重要的開始分析,因爲實現了Condition接口,因此await()和signal()就是切入點。
線程等待await()
我先整理出一個await()內部調用流程:
// 向隊列中添加節點並返回
private Node addConditionWaiter() {...}
// 釋放節點持有的鎖
final int fullyRelease(Node node) {...}
// 判斷節點是否在同步隊列中
final boolean isOnSyncQueue(Node node) {...}
// 檢查線程是否中斷,如果是則終止Condition狀態並加入到同步隊列
private int checkInterruptWhileWaiting(Node node) {...}
// 操作節點去申請鎖
final boolean acquireQueued(final Node node, int arg) { ...}
// 清理等待隊列中無效節點
private void unlinkCancelledWaiters() {...}
// 處理線程中斷情況
private void reportInterruptAfterWait(int interruptMode){...}
接下來挨個分析實現方法,先從入口await()方法開始。
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter(); // t1
int savedState = fullyRelease(node); // t2
int interruptMode = 0;
while (!isOnSyncQueue(node)) { // t3
LockSupport.park(this); //t4
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) //t5
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE) // t6
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters(); // t7
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode); // t8
}
在t1位置,先看一下addConditionWaiter()方法,看名字是增加了一個條件等待對象,應該就是向等待隊列中操作了。
private Node addConditionWaiter() {
Node t = lastWaiter; // 獲取尾部指針,看來是採用尾插法
// If lastWaiter is cancelled, clean out.
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters(); // t7'
t = lastWaiter;
}
Node node = new Node(Thread.currentThread(), Node.CONDITION); // 創建一個新節點
if (t == null) // 尾結點爲空,說明隊列是空的
firstWaiter = node; // 初始化隊列
else
t.nextWaiter = node; // 尾插
lastWaiter = node; // 調整尾指針指向
return node; // 返回新增節點對象
}
t7和t7',在await()和addConditionWaiter()方法中,都調用了unlinkCancelledWaiters(),先看一下它做了什麼:
private void unlinkCancelledWaiters() {
Node t = firstWaiter; // 拿到頭節點
Node trail = null;
while (t != null) { // 如果頭節點不爲空,隊列不爲空
Node next = t.nextWaiter; // 遍歷等待隊列
if (t.waitStatus != Node.CONDITION) { // 如果節點的狀態不是CONDITION
t.nextWaiter = null; // 將節點移除隊列
if (trail == null) // 首次遍歷,進度爲0
firstWaiter = next; // 頭節點指向被移除節點的下一個節點
else
trail.nextWaiter = next; // 進度指向下一個節點,也是將修復被移除隊列節點的影響,保證隊列連續
if (next == null)
lastWaiter = trail; // 如果next爲空,說明隊列遍歷完成,將尾指針指向進度節點
}
else // 如果節點的狀態是CONDITION
trail = t; // 保存進度
t = next;
}
}
那麼unlinkCancelledWaiters()方法就做了一件事,遍歷等待隊列,將非CONDITION狀態到的節點移除。
重點:在等待隊列中,我們發現獲取節點的後繼節點時,使用的是nextWaiter屬性,而非next,這就是區別“等待隊列”和“同步隊列”的關鍵。
- 在同步隊列中,獲取後繼節點採用的是next屬性。
- 在等待隊列中,獲取後繼節點採用的是nextWaiter屬性。
在addConditionWaiter()方法的t7'位置調用的目的:調用條件是t.waitStatus != Node.CONDITION,也就是同步隊列尾結點狀態不對,那麼這時清理一次同步隊列再插入新節點很有必要。
在await()方法的t7位置調用的目的:由於t7前面還有其他邏輯未介紹,這裏我們稍後繼續分析。(調用條件是node.nextWaiter != null)
說回addConditionWaiter()方法,它其實和addWaiter()方法功能差不多,向隊列中添加節點,這裏的隊列是“等待隊列”。接着分析await()方法。
int savedState = fullyRelease(node); // t2
在t2位置,調用fullyRelease(node),傳入新添加的node節點,並返回一個狀態:
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState(); // 獲取AQS中的state值
if (release(savedState)) { /// 調用釋放鎖方法
failed = false;
return savedState; // 如果釋放成功,返回state值
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
在fullyRelease()方法中,主要是調用了release()去釋放鎖。這裏有個前提就是線程必須先持有鎖,才能調用await()方法,進而release()釋放鎖。
那麼就引出了await()方法暫停線程,會導致鎖被釋放的邏輯。
release()方法的實現,前文有提到,需要回顧的請戳《面試必考AQS-排它鎖的申請與釋放》。我們繼續分析await()方法。
while (!isOnSyncQueue(node)) { // t3
LockSupport.park(this); //t4
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) //t5
break;
}
在t3位置,調用了while循環,條件是!isOnSyncQueue(node),是否不在同步隊列中? 如果不在,將會執行下面的內容。
final boolean isOnSyncQueue(Node node) {
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false; // 以節點狀態作爲判斷條件,如果等於CONDITION(說明在等待隊列中)、或者前置節點爲空,是一個獨立節點
if (node.next != null) // If has successor, it must be on queue
return true; // 如果後繼節點不爲空,說明它還在同步隊列中。
/*
* node.prev can be non-null, but not yet on queue because
* the CAS to place it on queue can fail. So we have to
* traverse from tail to make sure it actually made it. It
* will always be near the tail in calls to this method, and
* unless the CAS failed (which is unlikely), it will be
* there, so we hardly ever traverse much.
* 前置節點爲空,並不代表節點不在隊列上,因爲 CAS操作有可能失敗。 因此需要從尾部遍歷隊列來保證它不在隊列上。
*/
return findNodeFromTail(node); // 從尾部找到node節點
}
這裏要注意一點,node的next屬性是AQS的同步隊列範疇的屬性,在ConditionObject中是沒有使用next屬性的。這點在分析unlinkCancelledWaiters()方法時說明過。
// 這個方法沒什麼好解釋的
private boolean findNodeFromTail(Node node) {
Node t = tail;
for (;;) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
那麼,如果while (!isOnSyncQueue(node)) 成立,就是節點node不在同步隊列上,則說明node已經釋放鎖了,並且進入了等待隊列。接下來讓線程掛起、等待被喚醒就可以了。
LockSupport.park(this); //t4
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) //t5
break;
在t5位置,執行的條件是線程被喚醒,喚醒後首先要檢查的是,在這期間線程是否有被中斷,保證線程安全。
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) : // t5-1
0;
}
final boolean transferAfterCancelledWait(Node node) {
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) { // 將節點狀態由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.
* 如果忘記調用signal,那麼就不能繼續執行了,要讓它回到同步隊列中。
*/
while (!isOnSyncQueue(node)) // 判斷線程是否在同步隊列,直到回到同步隊列(取消,也要先讓node回到同步隊列)
Thread.yield(); // 讓出CPU時間
return false; // 修改node狀態失敗,返回false
}
在t5-1位置,如果線程爲中斷狀態,則進入transferAfterCancelledWait() ,裏面會操作node狀態由CONDITION回到初始狀態0,此時如果操作成功,會將node重新放回同步隊列。
如果CAS失敗,則需要向下執行,有可能是其他操作改變了node狀態,或許是取消的場景,因爲這裏進入的前提是線程已經被中斷。
在結束了transferAfterCancelledWait()方法後,根據返回的true/false,確定 返回THROW_IE還是REINTERRUPT狀態,如果沒有中斷則返回0,也就是interruptMode的初始值。
總結t5的邏輯,線程被喚醒後,檢查線程狀態,如果是中斷狀態,要嘗試將node的節點狀態變更爲0,如果變更成功,則判定中斷原因是異常,如果變更失敗,要給線程時間讓其他線程將node放回同步隊列。
在t5位置,如果返回的不是初始值,則外層while會被break;如果是初始值,則會判斷是否進入同步隊列,是則結束循環,否則說明還在等待隊列,需要繼續被掛起。
當循環結束,後續流程就需要 讓線程重新進入鎖競爭狀態,並且前面判斷了那麼多線程狀態,也要根據返回值處理一下。
if (acquireQueued(node, savedState) && interruptMode != THROW_IE) // t6
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters(); // t7
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode); // t8
在t6位置,讓節點線程再次去申請鎖,同時傳入掛起前保存的資源值saveState,節點回到競爭狀態後就是AQS申請邏輯,可以交給AQS了;對於await()來說,剩下的就是處理線程狀態了。
如果interruptMode != 異常,則調整interruptMode的值爲REINTERRUPT。也就是說,如果線程申請鎖成功,未來會讓線程中斷。
在t7位置,如果節點node有後繼節點,那麼需要將node從等待隊列移除
在t8位置,如果interruptMode的值不爲0,也就是不正常狀態,進入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()方法的全部流程,大致可歸納爲:
1、將持有鎖的線程包裝爲node,並放入等待隊列
2、將持有的鎖釋放,保持持有鎖時申請的資源值
3、循環判斷節點node釋放在同步隊列中,如果沒有則掛起線程
4、線程被喚醒後,要判斷線程狀態
5、讓線程去申請鎖,根據申請規則,如果申請失敗會在同步隊列掛起
6、如果申請成功,要根據線程狀態對線程進行合理的處理:拋異常或中斷
線程喚醒signal()
先整理出一個signal()內部調用流程:
public final void signal() {...} // 喚醒線程入口
protected boolean isHeldExclusively(); // 判定當前線程是否持有鎖
private void doSignal(Node first) {...} // 喚醒first節點
final boolean transferForSignal(Node node) {...} // 轉換節點狀態
從入口方法signnal()來分析:
public final void signal() {
if (!isHeldExclusively()) // 抽象方法,有子類實現,用於判斷當前線程是否持有鎖
throw new IllegalMonitorStateException(); // 只有持有鎖的線程才能操作喚醒
Node first = firstWaiter; // 獲取等待隊列的頭結點
if (first != null)
doSignal(first); // 執行喚醒操作
}
入口就是一些狀態判斷,真正執行喚醒的是doSignal()方法:
private void doSignal(Node first) {
do {
// t1
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) && // t2
(first = firstWaiter) != null); // t3
}
方法進入後,遇到一個do..while循環,先執行do內邏輯。
在t1位置,判斷給定的節點first是否存在後繼節點,如果不存在,將lastWaiter置爲null。這裏就是將等待隊列清空。
接着進入t2位置,調用transferForSignal()方法:
final boolean transferForSignal(Node node) {
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) // 將節點狀態恢復爲0,如果修改失敗返回false
return false; // t2-1
/*
* 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)) // t2-2,或者,無法調整爲SIGNAL
LockSupport.unpark(node.thread); // 喚醒線程
return true;
}
在t2-1位置,將節點狀態恢復爲0,如果修改失敗返回false。
在t2-2位置,或的判斷,兩個條件:
1、如果同步狀態爲取消,則喚醒線程,在await()邏輯中,被喚醒的線程會檢查線程狀態,此時的取消會導致在transferAfterCancelledWait()方法中,無法將node狀態由CONDITION轉爲0,也就進而不停讓出線程cpu時間,導致線程被取消。
2、如果compareAndSetWaitStatus(p, ws, Node.SIGNAL)==false,也就是CA無法改變ws值,就說明有其他線程在操作該node。
以上兩種條件都必須要喚醒線程。
while (!transferForSignal(first) && // t2
(first = firstWaiter) != null); // t3
當然以上兩種條件有可能都不成立,那麼就繼續在t2位置執行循環,直到條件成立。
當執行了t2-1位置,也就代表節點node狀態被重置,並且已經從等待隊列出隊,那麼,在t3位置==遍歷等待隊列下一個節點。
在while條件中,完整邏輯是:不斷嘗試喚醒等待隊列的頭節點,直到找到一個沒有被cancel的節點,跳出循環。
以上就是signal()方法的所有源碼,歸納一下:
1、只有持有鎖的線程才能操作喚醒
2、喚醒時要針對 等待隊列 的頭節點所代表的線程
3、喚醒= 線程節點node 狀態重置 + node回到同步隊列 + unpark線程
4、喚醒過程中如果遇到cancel狀態的節點,要嘗試等待隊列中下一個,直到找到可被正常喚醒節點 或者 隊列爲空
喚醒所有線程signalAll()
在Condition接口中,還有一個signalAll()方法,目的是喚醒所有等待的節點,來分析一下源碼:
public final void signalAll() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignalAll(first);
}
入口方法看了與signal()差不多,只是最後執行的方法是doSignalAll()。
private void doSignalAll(Node first) {
lastWaiter = firstWaiter = null;
do {
Node next = first.nextWaiter;
first.nextWaiter = null;
transferForSignal(first);
first = next;
} while (first != null);
}
與doSignal()的區別是 while流程有變化,它不是找到一個可被喚醒的節點就結束,而是遍歷整個等待隊列,將所有節點喚醒。
尾聲
Condition及ConditionObject,實現了線程的等待與喚醒行爲,在併發編程中,熟練使用它們能夠大大提升並行效率,減少線程空轉,降低CPU消耗。
自此,AQS的主要源碼已經分析完畢,後面會挑選JUC下的主要實現類做分析,來看一下之前反覆提到的tryxxx()方法是如何實現以達到不同特色、不同類型的鎖的。
推薦閱讀: