AQS 都看完了,Condition 原理可不能少!

流小航發表於2020-10-01

前言


在介紹 AQS 時,其中有一個內部類叫做 ConditionObject,當時並沒有進行介紹,並且在後續閱讀原始碼時,會發現很多地方用到了 Condition ,這時就會很詫異,這個 Condition 到底有什麼作用?那今天就通過閱讀 Condition 原始碼,從而弄清楚 Condition 到底是做什麼的?當然閱讀這篇文章的時候希望你已經閱讀了 AQS、ReentrantLock 以及 LockSupport 的相關文章或者有一定的瞭解(當然小夥伴也可以直接跳到文末看總結)。


公眾號:liuzhihangs,記錄工作學習中的技術、開發及原始碼筆記;時不時分享一些生活中的見聞感悟。歡迎大佬來指導!

介紹

Object 的監視器方法:wait、notify、notifyAll 應該都不陌生,在多執行緒使用場景下,必須先使用 synchronized 獲取到鎖,然後才可以呼叫 Object 的 wait、notify。

Condition 的使用,相當於用 Lock 替換了 synchronized,然後用 Condition 替換 Object 的監視器方法。

Conditions(也稱為條件佇列或條件變數)為一種執行緒提供了一種暫停執行(等待),直到另一執行緒通知被阻塞的執行緒,某些狀態條件現在可能為真。

因為訪問到此共享狀態資訊發生在不同的執行緒中,因此必須對其進行保護,所以會使用某種形式的鎖。等待條件提供的關鍵屬性是它以原子地釋放了關聯的鎖,並且掛起當前執行緒,就像 Object.wait 一樣。

Condition 例項本質上要繫結到鎖。 為了獲得 Condition 例項,一般使用 Lock 例項的 newCondition() 方法。

Lock lock = new ReentrantLock();
Condition con = lock.newCondition();

基本使用

class BoundedBuffer {

    final Lock lock = new ReentrantLock();
    // condition 例項依賴於 lock 例項
    final Condition notFull = lock.newCondition();
    final Condition notEmpty = lock.newCondition();

    final Object[] items = new Object[100];

    int putPtr, takePtr, count;

    public void put(Object x) throws InterruptedException {
        lock.lock();
        try {
            //  put 時判斷是否已經滿了
            // 則執行緒在 notFull 條件上排隊阻塞
            while (count == items.length) {
                notFull.await();
            }
            items[putPtr] = x;
            if (++putPtr == items.length) {
                putPtr = 0;
            }
            ++count;
            // put 成功之後,佇列中有元素
            // 喚醒在 notEmpty 條件上排隊阻塞的執行緒
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public Object take() throws InterruptedException {
        lock.lock();
        try {
            // take 時,發現為空
            // 則執行緒在 notEmpty 的條件上排隊阻塞
            while (count == 0) {
                notEmpty.await();
            }
            Object x = items[takePtr];
            if (++takePtr == items.length) {
                takePtr = 0;
            }
            --count;
            // take 成功,佇列不可能是滿的
            // 喚醒在 notFull 條件上排隊阻塞的執行緒
            notFull.signal();
            return x;
        } finally {
            lock.unlock();
        }
    }
}

上面是官方文件的一個例子,實現了一個簡單的 BlockingQueue ,看懂這裡,會發現在同步佇列中很多地方都是用的這個邏輯。必要的程式碼說明都已經在程式碼中進行註釋。

問題疑問

  1. Condition 和 AQS 有什麼關係?
  2. Condition 的實現原理是什麼?
  3. Condition 的等待佇列和 AQS 的同步佇列有什麼區別和聯絡?

原始碼分析

基本結構

condition-uml-rMKuf3

通過 UML 可以看出,Condition 只是一個抽象類,它的主要實現邏輯是在 AQS 的內部類 ConditionObject 實現的。下面主要從 await 和 signal 兩個方法入手,從原始碼瞭解 ConditionObject。

建立 Condition

Lock lock = new ReentrantLock();
Condition con = lock.newCondition();

一般使用 lock.newCondition() 建立條件變數。

public class ReentrantLock implements Lock, java.io.Serializable {

    private final Sync sync;

    public Condition newCondition() {
        return sync.newCondition();
    }
    // Sync 整合 AQS
    abstract static class Sync extends AbstractQueuedSynchronizer {
        
        final ConditionObject newCondition() {
            return new ConditionObject();
        }
    }
}

這裡使用的是 ReentrantLock 的原始碼,裡面呼叫的 sync.newCondition(),Sync 繼承 AQS,其實就是建立了一個 AQS 內部類的 ConditionObject 的例項。

這裡需要注意的是 lock 每呼叫一次 lock.newCondition() 都會有一個新的 ConditionObject 例項生成,就是說一個 lock 可以建立多個 Condition 例項。

Condition 引數

/** 條件佇列的第一個節點 */
private transient Node firstWaiter;
/** 條件佇列的最後一個節點 */
private transient Node lastWaiter;

await 方法

await 方法,會造成當前執行緒在等待,直到收到訊號或被中斷。

與此 Condition 相關聯的鎖被原子釋放,並且出於執行緒排程目的,當前執行緒被禁用,並且處於休眠狀態,直到發生以下四種情況之一:

  1. 其他一些執行緒呼叫此 Condition 的 signal 方法,而當前執行緒恰好被選擇為要喚醒的執行緒;
  2. 其他一些執行緒呼叫此 Condition 的 signalAll 方法;
  3. 其他一些執行緒中斷當前執行緒,並支援中斷執行緒掛起;
  4. 發生虛假喚醒。

在所有情況下,在此方法可以返回之前,當前執行緒必須重新獲取與此條件關聯的鎖。當執行緒返回時,可以保證保持此鎖。

現在來看 AQS 內部的實現邏輯:

public final void await() throws InterruptedException {
    // 響應中斷
    if (Thread.interrupted())
        throw new InterruptedException();
    // 新增到條件佇列尾部(等待佇列)
    // 內部會建立 Node.CONDITION 型別的 Node
    Node node = addConditionWaiter();
    // 釋放當前執行緒獲取的鎖(通過操作 state 的值)
    // 釋放了鎖就會被阻塞掛起
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    // 節點已經不在同步佇列中,則呼叫 park 讓其在等待佇列中掛著
    while (!isOnSyncQueue(node)) {
        // 呼叫 park 阻塞掛起當前執行緒
        LockSupport.park(this);
        // 說明 signal 被呼叫了或者執行緒被中斷,校驗下喚醒原因
        // 如果因為終端被喚醒,則跳出迴圈
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // while 迴圈結束, 執行緒開始搶鎖
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    // 統一處理中斷的
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

await 方法步驟如下:

  1. 建立 Node.CONDITION 型別的 Node 並新增到條件佇列(ConditionQueue)的尾部;
  2. 釋放當前執行緒獲取的鎖(通過操作 state 的值)
  3. 判斷當前執行緒是否在同步佇列(SyncQueue)中,不在的話會使用 park 掛起。
  4. 迴圈結束之後,說明已經已經在同步佇列(SyncQueue)中了,後面等待獲取到鎖,繼續執行即可。

在這裡一定要把條件佇列和同步佇列進行區分清楚!!

條件佇列/等待佇列:即 Condition 的佇列
同步佇列:AQS 的佇列。

下面對 await 裡面重要方法進行閱讀:

  • addConditionWaiter() 方法
private Node addConditionWaiter() {
    Node t = lastWaiter;
    // If lastWaiter is cancelled, clean out.
    // 判斷尾節點狀態,如果被取消,則清除所有被取消的節點
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    // 建立新節點,型別為 Node.CONDITION
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    // 將新節點放到等待佇列尾部
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

addConditionWaiter 方法可以看出,只是建立一個型別為 Node.CONDITION 的節點並放到條件佇列尾部。同時通過這段程式碼還可以得出其他結論:

  1. 條件佇列內部的 Node,只用到了 thread、waitStatus、nextWaiter 屬性;
  2. 條件佇列是單向佇列。

作為對比,這裡把條件佇列和同步佇列做出對比:

condition-node-7yUQjE

AQS 同步佇列如下:

condition-aqs-n5Fs85

再來看下 Condition 的條件佇列

condition-condition-A97bUS

waitStatus 在 AQS 中已經進行了介紹:

  1. 預設狀態為 0;
  2. waitStatus > 0 (CANCELLED 1) 說明該節點超時或者中斷了,需要從佇列中移除;
  3. waitStatus = -1 SIGNAL 當前執行緒的前一個節點的狀態為 SIGNAL,則當前執行緒需要阻塞(unpark);
  4. waitStatus = -2 CONDITION -2 :該節點目前在條件佇列;
  5. waitStatus = -3 PROPAGATE -3 :releaseShared 應該被傳播到其他節點,在共享鎖模式下使用。
  • fullyRelease 方法 (AQS)
final int fullyRelease(Node node) {
    boolean failed = true;
    try {
        // 獲取當前節點的 state
        int savedState = getState();
        // 釋放鎖
        if (release(savedState)) {
            failed = false;
            return savedState;
        } else {
            throw new IllegalMonitorStateException();
        }
    } finally {
        if (failed)
            node.waitStatus = Node.CANCELLED;
    }
}

fullyRelease 方法是由 AQS 提供的,首先獲取當前的 state,然後呼叫 release 方法進行釋放鎖。

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

release 方法在 AQS 中做了詳細的介紹。它的主要作用就是釋放鎖,並且需要注意的是:

  1. fullyRelease 會一次性釋放所有的鎖,所以說不管重入多少次,在這裡都會全部釋放的。
  2. 這裡會丟擲異常,主要是在釋放鎖失敗時,這時就會在 finally 裡面將節點狀態置為 Node.CANCELLED。
  • isOnSyncQueue(node)

通過上面的流程,節點已經放到了條件佇列並且釋放了持有的,而後就會掛起阻塞,直到 signal 喚醒。但是在掛起時要保證節點已經不在同步佇列(SyncQueue)中了才可以掛起。

final boolean isOnSyncQueue(Node node) {
    // 當前節點是條件佇列節點,或者上一個節點是空
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;
    if (node.next != null) // If has successor, it must be on queue
        return true;

    return findNodeFromTail(node);
}
// 從尾部開始遍歷
private boolean findNodeFromTail(Node node) {
    Node t = tail;
    for (;;) {
        if (t == node)
            return true;
        if (t == null)
            return false;
        t = t.prev;
    }
}

如果一個節點(總是一個最初放置在條件佇列中的節點)現在正等待在同步佇列上重新獲取,則返回true。

這段程式碼的主要作用判斷節點是不是在同步佇列中,如果不在同步佇列中,後面才會呼叫 park 進行阻塞當前執行緒。這裡就會有一個疑問:AQS 的同步佇列和 Condition 的條件佇列應該是無關的,這裡為什麼會要保證節點不在同步佇列之後才可以進行阻塞?因為 signal 或者 signalAll 喚醒節點之後,節點就會被放到同步佇列中。

執行緒到這裡已經被阻塞了,當有其他執行緒呼叫 signal 或者 signalAll 時,會喚醒當前執行緒。

而後會驗證是否因中斷喚醒當前執行緒,這裡假設沒有發生中斷。那 while 迴圈的 isOnSyncQueue(Node node) 必然會返回 true ,表示當前節點已經在同步佇列中了。

後續會呼叫 acquireQueued(node, savedState) 進行獲取鎖。

final boolean acquireQueued(final Node node, int arg) {
    // 是否拿到資源
    boolean failed = true;
    try {
        // 中斷狀態
        boolean interrupted = false;
        // 無限迴圈
        for (;;) {
            // 當前節點之前的節點
            final Node p = node.predecessor();
            // 前一個節點是頭節點, 說明當前節點是 頭節點的 next 即真實的第一個資料節點 (因為 head 是虛擬節點)
            // 然後再嘗試獲取資源
            if (p == head && tryAcquire(arg)) {
                // 獲取成功之後 將頭指標指向當前節點
                setHead(node); 
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // p 不是頭節點, 或者 頭節點未能獲取到資源 (非公平情況下被別的節點搶佔) 
            // 判斷 node 是否要被阻塞,獲取不到鎖就會一直阻塞
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

這裡就是 AQS 的邏輯了,同樣可以閱讀 AQS 的相關介紹。

  1. 不斷獲取本節點的上一個節點是否為 head,因為 head 是虛擬節點,如果當前節點的上一個節點是 head 節點,則當前節點為 第一個資料節點>
  2. 第一個資料節點不斷的去獲取資源,獲取成功,則將 head 指向當前節點;
  3. 當前節點不是頭節點,或者 tryAcquire(arg) 失敗(失敗可能是非公平鎖)。這時候需要判斷前一個節點狀態決定當前節點是否要被阻塞(前一個節點狀態是否為 SIGNAL)。

值得注意的是,當節點放到 AQS 的同步佇列時,也是進行爭搶資源,同時設定 savedState 的值,這個值則是代表當初釋放鎖的時候釋放了多少重入次數。

總體流程畫圖如下:

condition-await-q4EBQx

signal

public final void signal() {
    // 是否為當前持有執行緒
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

private void doSignal(Node first) {
    do {
        // firstWaiter 頭節點指向條件佇列頭的下一個節點
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        // 將原來的頭節點和同步佇列斷開
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&
                (first = firstWaiter) != null);
}

final boolean transferForSignal(Node node) {
 
    // 判斷節點是否已經在之前被取消了
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    // 呼叫 enq 新增到 同步佇列的尾部
    Node p = enq(node);
    int ws = p.waitStatus;
    // node 的上一個節點 修改為 SIGNAL 這樣後續就可以喚醒自己了
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

enq 同樣可以閱讀 AQS 的程式碼

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        // 尾節點為空 需要初始化頭節點,此時頭尾節點是一個
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            // 不為空 迴圈賦值
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

通過 enq 方法將節點放到 AQS 的同步佇列之後,要將 node 的前一個節點的 waitStatus 設定為 Node.SIGNAL。signalAll 的程式碼也是類似。

總結

Q&A

Q: Condition 和 AQS 有什麼關係?

A: Condition 是基於 AQS 實現的,Condition 的實現類 ConditionObject 是 AQS 的一個內部類,在裡面共用了一部分 AQS 的邏輯。

Q: Condition 的實現原理是什麼?

A: Condition 內部維護一個條件佇列,在獲取鎖的情況下,執行緒呼叫 await,執行緒會被放置在條件佇列中並被阻塞。直到呼叫 signal、signalAll 喚醒執行緒,此後執行緒喚醒,會放入到 AQS 的同步佇列,參與爭搶鎖資源。

Q: Condition 的等待佇列和 AQS 的同步佇列有什麼區別和聯絡?
A: Condition 的等待佇列是單向連結串列,AQS 的是雙向連結串列。二者之間並沒有什麼明確的聯絡。僅僅在節點從阻塞狀態被喚醒後,會從等待佇列挪到同步佇列中。

結束語

本文主要是閱讀 Condition 的相關程式碼,不過省略了執行緒中斷等邏輯。有興趣的小夥伴。可以更深入的研究相關的原始碼。


作者:劉志航,一個宅宅的北漂程式設計師。

" 學,而知不足;教,然後知困。"

相關文章