面試必考AQS-await和signal的實現原理

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()方法是如何實現以達到不同特色、不同類型的鎖的。

 

推薦閱讀:

面試必考AQS-AQS概覽

面試必考AQS-AQS源碼全局分析

面試必考AQS-排它鎖的申請與釋放

面試必考AQS-共享鎖申請、釋放及傳播狀態

面試必考AQS-await和signal的實現原理

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章