併發程式設計之 Condition 原始碼分析

莫那·魯道發表於2018-04-30

前言

Condition 是 Lock 的伴侶,至於如何使用,我們之前也寫了一些文章來說,例如 使用 ReentrantLock 和 Condition 實現一個阻塞佇列併發程式設計之 Java 三把鎖, 在這兩篇文章中,我們都詳細介紹了他們的使用。今天我們就來深入看看原始碼實現。

構造方法

Condition 介面有 2 個實現類,一個是 AbstractQueuedSynchronizer.ConditionObject,還有一個是 AbstractQueuedLongSynchronizer.ConditionObject,都是 AQS 的內部類,該類結構如下:

image.png

幾個公開的方法:

  1. await()
  2. await(long time, TimeUnit unit)
  3. awaitNanos(long nanosTimeout)
  4. awaitUninterruptibly()
  5. awaitUntil(Date deadline)
  6. signal()
  7. 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 的過程吧。

  1. 首先,執行緒如果想執行 await 方法,必須拿到鎖,在 AQS 裡面,搶到鎖的一般都是 head,然後 head 失效,從佇列中刪除。

  2. 在當前執行緒(也就是 AQS 的 head)拿到鎖後,呼叫了 await 方法,第一步建立一個 Node 節點,放到 Condition 自己的佇列尾部,並喚醒 AQS 佇列中的某個(head)節點,然後阻塞自己,等待被 signal 喚醒。

  3. 當有其他執行緒呼叫了 signal 方法,就會喚醒 Condition 佇列中的 first 節點,然後將這個節點放進 AQS 佇列的尾部。

  4. 阻塞在 await 方法的執行緒甦醒後,他已經從 Condition 佇列總轉移到 AQS 佇列中了,這個時候,他就是一個正常的 AQS 節點,就會嘗試搶鎖。並清除 Condition 佇列中無效的節點。

下面這張圖說明了這幾個步驟。

image.png

好啦,關於 Condition 就說到這裡啦,總的來說,就是在 Condition 中新增一個休眠佇列來實現的。只要呼叫 await 方法,就會休眠,進入 Condition 佇列,呼叫 signal 方法,就會從 Condition 佇列中取出一個執行緒並插入到 AQS 佇列中,然後喚醒,讓這個執行緒自己去搶鎖。

相關文章