全網最詳細的AbstractQueuedSynchronizer(AQS)原始碼剖析(三)條件變數

酒冽發表於2021-12-22

前兩期我們已經掌握了AQS的基本結構、以及AQS是如何釋放和獲取資源的。其實到這裡,我們已經掌握了AQS作為同步器的全部功能
不過,有些情況使用同步功能不夠靈活,所以AQS又引入了作業系統中的另一個高度相關的概念——條件變數。由於條件變數的使用緊密依賴於AQS提供的釋放、獲取資源功能和同步佇列,因此都放在了AQS原始碼中
能堅持看到這裡的同學已經很不容易了,再接再厲,一起沖掉最後一座堡壘吧???

簡介

條件變數是什麼

條件物件這一概念源自於作業系統,設計它是為了解決等待同步需求,實現執行緒間協作通訊的一種機制。Java其實也已經內建了條件變數,它和監視器鎖是繫結在一起的,即Object的wait和notify方法,使用這兩個方法就可以實現執行緒之間的協作

Java中的條件變數直到Java 5才出現,用它來代替傳統的Object的wait和notify方法。相比wait和notify,Condition的await和signal方法更加安全和高效,因為Condition是基於AQS實現的,加鎖、釋放鎖的效率更高

條件變數顧名思義就是表示某種條件的變數。不過需要說明的是,條件變數中的條件並沒有實際含義,僅僅只是一個標記,條件的含義需要程式碼來賦予

Condition介面

Condition是一個介面,條件變數都實現了Condition介面。該介面的基本方法是await、signal、signalAll這些

不過Condition依賴於Lock介面,需要藉助Lock.newCondition方法來建立條件變數。因此Condition必然是和某個Lock繫結在一起的,這就和await和signal一定會和Object的監視器鎖繫結在一起。因此,Java中的條件變數必須配合鎖使用,來控制併發程式訪問競爭資源的執行緒安全性

Condition和Java內建的條件變數方法之間的對應關係如下:

  • Condition.await 等價於 Object.wait
  • Condition.signal 等價於 Object.notify

不多BB了,直接看原始碼吧~

AQS之條件物件

AQS為條件變數的實現提供了95%以上的功能,Lock介面實現類一般只需要實現一下newCondition方法,就可以直接使用條件變數了,你說方不方便!

AQS實現的方式就是直接提供了Condition的一個實現類——ConditionObject,我接下來就將其稱為條件物件(可能不太嚴謹)。ConditionObject是AQS的內部類,可見性為public

條件物件的結構

每個ConditionObject都維護了一個條件佇列,首尾節點分別由firstWaiterlastWaiter兩個域來管理:

/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;

每個在條件佇列中等待的執行緒都由Node類來維護,它們之間通過Node類的nextWaiter相連,形成一個單向連結串列。每個Node的waitStatus都是Node.CONDITION,表明該執行緒正在某個條件變數的條件佇列中等待ing~

條件物件的建立

十分的樸實無華且簡單通透,要是所有程式碼都這麼簡單,那該有多好啊~

public ConditionObject() { }

正如前面所說,如果基於AQS實現的Lock介面實現類想要使用AQS提供的條件變數,只需要實現newCondition方法即可。而實現newCondition方法一般只需要直接呼叫ConditionObject的構造方法即可,多麼簡單!

接下來主要剖析一下ConditionObject是如何實現Condition介面的兩大重要功能——條件等待、條件喚醒

作者:酒冽        出處:https://www.cnblogs.com/frankiedyz/p/15676704.html
版權:本文版權歸作者和部落格園共有
轉載:歡迎轉載,但未經作者同意,必須保留此段宣告;必須在文章中給出原文連線;否則必究法律責任

條件等待

await

await方法就是最基本的條件等待,它是可以被中斷的。原始碼如下:

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // 能夠離開while迴圈,說明被signal或被中斷了,而且在同步佇列中,所以可以呼叫acquireQueued
    // 而且之前釋放了多少資源,現在就要還原回來,因此獲取的引數是之前儲存的state
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

addConditionWaiter方法會新增一個新節點到等待佇列的隊尾,該節點的waitStatus是CONDITION。原始碼如下:

private Node addConditionWaiter() {
    Node t = lastWaiter;
    // 如果lastWaiter被取消(CANCELLED),就移除它以及前面所有被取消的節點
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    Node node = new Node(Thread.currentThread(), Node.CONDITION);		// 為當前執行緒建立一個Node,waitStatus為CONDITION
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

unlinkCancelledWaiters會將所有被取消的Node從條件佇列中移除,該方法只會在持有鎖的情況下才會被呼叫,原始碼如下:

private void unlinkCancelledWaiters() {
    Node t = firstWaiter;
    Node trail = null;
    while (t != null) {
        Node next = t.nextWaiter;
        if (t.waitStatus != Node.CONDITION) {
            t.nextWaiter = null;
            if (trail == null)
                firstWaiter = next;
            else
                trail.nextWaiter = next;
            if (next == nul)
                lastWaiter = trail;
        }
        else
            trail = t;
        t = next;
    }
}

unlinkCancelledWaiters相當於是進行了一次正向遍歷,將那些waitStatus不為CONDITION(即為CANCELLED)的Node給移除,挺簡單的~

回到await方法,接下來呼叫fullyRelease方法儲存當前的state,並呼叫一次release,最後返回儲存的state。原始碼如下:

final int fullyRelease(Node node) {
    boolean failed = true;
    try {
        int savedState = getState();
        if (release(savedState)) {
            failed = false;
            return savedState;
        } else {
            throw new IllegalMonitorStateException();
        }
    } finally {
        if (failed)
            node.waitStatus = Node.CANCELLED;	// 如果失敗就會將節點取消
    }
}

呼叫release就是為了完全釋放當前執行緒所持有的資源,防止死鎖。如果釋放失敗,說明當前執行緒壓根沒有獲取到資源就呼叫了await方法,這是不允許的,會丟擲IllegalMonitorStateException異常。這也解決了我之前的疑惑——如何保證呼叫unlinkCancelledWaiters之前必須持有鎖呢? 這裡就給出了答案!只有在當前執行緒持有資源的前提下,程式碼才能正常執行下去

回到await方法,接下來呼叫isOnSyncQueue方法來檢查當前執行緒對應的Node在同步佇列上還是條件佇列上,原始碼如下:

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);
}

該方法會先看目標node的waitStatus是否為CONDITION,或者它的prev是否為空,如果是,那麼它一定在條件佇列上,返回false;如果目標node的next域不為空,那麼它一定在同步佇列上,返回true。如果以上兩種快捷判斷方法無法判定,那麼需要呼叫findNodeFromTail進行暴力判定。總之,如果目標node在條件佇列上等待,則返回true,否則說明在同步佇列上,返回false

await中的while(!isOnSyncQueue)語義:如果當前節點所等待的條件沒有滿足,那麼說明它還在條件佇列上等待,就會一直被困在while迴圈中不停地被阻塞,不能離開while迴圈去競爭獲取資源

findNodeFromTail方法會從tail開始向前遍歷,以確定目標node是否位於同步佇列上,很簡單哦~其原始碼如下:

// 此方法只會被isOnSyncQueue方法呼叫
private boolean findNodeFromTail(Node node) {
    Node t = tail;
    for (;;) {
        if (t == node)
            return true;
        if (t == null)
            return false;
        t = t.prev;
    }
}

回到await方法,只有當node在條件佇列上等待,才會執行while迴圈。在迴圈中,將當前執行緒(即node對應的執行緒)阻塞。如果被喚醒,就呼叫checkInterruptWhileWaiting方法檢查是否在阻塞過程中被中斷,如果發生了中斷,該方法會返回非0值。while迴圈中如果發現被中斷,則退出迴圈

checkInterruptWhileWaiting方法即其呼叫到的transferAfterCancelledWait方法,原始碼如下:

/*
 * 不同返回值的含義:
 * 		0:沒有發生中斷
 * 		THROW_IE(-1):中斷髮生在被signal之前
 * 		REINTERRUPT(1):中斷髮生在被signal之後
 */
private int checkInterruptWhileWaiting(Node node) {
    return Thread.interrupted() ?
        (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
    0;
}

/*
 * transferAfterCancelledWait有兩個作用:
 * 1、將目標node放到同步佇列上
 * 2、檢測node是否已經被signal,true表示還未被signal
final boolean transferAfterCancelledWait(Node node) {
    // 如果CAS成功,說明該node還未接收到signal訊號
    if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
        enq(node);
        return true;
    }
    // 如果上面if中的CAS失敗,說明node已經被signal
    // 那麼就等待signal中的邏輯將node移動到同步佇列上去,這裡什麼也不做,自旋等待即可
    while (!isOnSyncQueue(node))
        Thread.yield();
    return false;
}

總之,如果執行緒在阻塞過程中沒有發生中斷,則checkInterruptWhileWaiting返回0,否則返回非0值
具體來說,如果發生中斷,就會呼叫transferAfterCancelledWait方法將node轉移到同步佇列,並檢測中斷是發生在被signal之前還是之後。如果發生在被signal之前則返回THROW_IE(-1),如果發生在被signal之後則返回REINTERRUPT(1)

回到await方法的while迴圈中,如果沒有被signal或被中斷就會一直阻塞。如果被signal就不滿足while條件,會退出迴圈;如果被中斷而不是被signal,會直接break離開while迴圈
退出while迴圈說明此時已經位於同步佇列(要麼因為被signal而transfer到同步佇列,要麼因為被中斷而呼叫transferAfterCancelledWait方法移動到同步佇列),可以直接呼叫acquireQueued方法(呼叫該方法的前提:位於同步佇列中)排隊獲取鎖,獲取雖然可能導致再次被阻塞,但這裡是在同步佇列上的阻塞,而不是在條件佇列上的阻塞

退出處理是通過reportInterruptAfterWait方法,具體如何處理,取決於interruptMode的值。原始碼如下:

private void reportInterruptAfterWait(int interruptMode) throws InterruptedException {
    if (interruptMode == THROW_IE)
        throw new InterruptedException();
    else if (interruptMode == REINTERRUPT)
        selfInterrupt();
}

如果interruptMode是THROW_IE,那麼說明該執行緒並未真正收到signal訊號,只是因為被中斷而被喚醒,所等待的條件並不一定被滿足,於是丟擲中斷異常;如果是REINTERRUPT,呼叫selfInterrupt方法設定執行緒的中斷狀態

總的來說,await執行分為6步:
1、如果當前執行緒中斷,那麼丟擲中斷異常
2、為當前執行緒建立Node,並加入該條件變數的條件佇列隊尾
3、獲取並儲存下當前state,並使用儲存的state來呼叫release方法,如果失敗則丟擲IllegalMonitorStateException異常
4、阻塞直到被signal或被中斷
5、利用儲存的state,呼叫acquireQueued方法重新獲取狀態
6、如果第四步期間被中斷,則丟擲中斷異常

超時await

await方法有一個過載版本,可以設定超時引數,如果超時也會導致等待停止。其原始碼如下:

public final boolean await(long time, TimeUnit unit)
    throws InterruptedException {
    long nanosTimeout = unit.toNanos(time);
    if (Thread.interrupted())
        throw new InterruptedException();
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);
    final long deadline = System.nanoTime() + nanosTimeout;
    boolean timedout = false;
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        if (nanosTimeout <= 0L) {
            timedout = transferAfterCancelledWait(node);
            break;
        }
        if (nanosTimeout >= spinForTimeoutThreshold)
            LockSupport.parkNanos(this, nanosTimeout);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
        nanosTimeout = deadline - System.nanoTime();
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null)
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
    return !timedout;
}

帶超時引數的await和無引數await基本差不多,執行一共分6步,不過最後一步會返回是否超時: 1、如果當前執行緒中斷,那麼丟擲中斷異常 2、為當前執行緒建立Node,並加入該條件變數的條件佇列隊尾 3、獲取並儲存下當前state,並使用儲存的state來呼叫release方法,如果失敗則丟擲IllegalMonitorStateException異常 4、阻塞直到被signal或被中斷,或超時 5、利用儲存的state,呼叫acquireQueued方法重新獲取狀態 6、如果第四步期間被中斷,則丟擲中斷異常;如果第四步是因為超時而停止阻塞,則返回false,否則返回true

awaitUninterruptibly

呼叫awaitUninterruptibly方法在等待過程中,不會因為中斷而停止等待。原始碼如下:

public final void awaitUninterruptibly() {
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);
    boolean interrupted = false;
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if (Thread.interrupted())
            interrupted = true;
    }
    if (acquireQueued(node, savedState) || interrupted)
        selfInterrupt();
}

awaitUninterruptibly執行分4步: 1、為當前執行緒建立Node,並加入該條件變數的條件佇列隊尾 2、獲取並儲存下當前state,並使用儲存的state來呼叫release方法,如果失敗則丟擲IllegalMonitorStateException異常 3、阻塞直到被signal 4、利用儲存的state,呼叫acquireQueued方法重新獲取狀態

awaitNanos

awaitNanos最多等待一定時間,可以被中斷。其原始碼如下:

public final long awaitNanos(long nanosTimeout)
    throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);
    final long deadline = System.nanoTime() + nanosTimeout;
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        // 如果超時,則將node從條件佇列轉移到同步佇列上去,並退出while迴圈
        if (nanosTimeout <= 0L) {
            transferAfterCancelledWait(node);
            break;
        }
        if (nanosTimeout >= spinForTimeoutThreshold)
            LockSupport.parkNanos(this, nanosTimeout);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
        nanosTimeout = deadline - System.nanoTime();
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null)
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
    return deadline - System.nanoTime();
}

awaitNanos執行分6步: 1、如果當前執行緒中斷,那麼丟擲中斷異常 2、為當前執行緒建立Node,並加入該條件變數的條件佇列隊尾 3、獲取並儲存下當前state,並使用儲存的state來呼叫release方法,如果失敗則丟擲IllegalMonitorStateException異常 4、阻塞直到被signal或被中斷或超時 5、利用儲存的state,呼叫acquireQueued方法重新獲取狀態 6、如果第四步期間被中斷,則丟擲中斷異常;最後返回距離超時還剩多少納秒

awaitUntil

類似於awaitNanos,會返回是否是因為超時而終止等待。原始碼如下:

public final boolean awaitUntil(Date deadline)
    throws InterruptedException {
    long abstime = deadline.getTime();
    if (Thread.interrupted())
        throw new InterruptedException();
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);
    boolean timedout = false;
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        if (System.currentTimeMillis() > abstime) {
            timedout = transferAfterCancelledWait(node);
            break;
        }
        LockSupport.parkUntil(this, abstime);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null)
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
    return !timedout;
}

awaitUntil和awaitNanos基本差不多,執行一共分6步,不過最後一步會返回是否超時: 1、如果當前執行緒中斷,那麼丟擲中斷異常 2、為當前執行緒建立Node,並加入該條件變數的條件佇列隊尾 3、獲取並儲存下當前state,並使用儲存的state來呼叫release方法,如果失敗則丟擲IllegalMonitorStateException異常 4、阻塞直到被signal或被中斷或超時 5、利用儲存的state,呼叫acquireQueued方法重新獲取狀態 6、如果第四步期間被中斷,則丟擲中斷異常;如果第四步是因為超時而停止阻塞,則返回false,否則返回true
作者:酒冽        出處:https://www.cnblogs.com/frankiedyz/p/15676704.html
版權:本文版權歸作者和部落格園共有
轉載:歡迎轉載,但未經作者同意,必須保留此段宣告;必須在文章中給出原文連線;否則必究法律責任

條件喚醒

signal

signal用於喚醒某個等待在條件佇列的執行緒。原始碼如下:

public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

首先呼叫isHeldExclusively方法來判斷當前執行緒是否持有資源(isHeldExclusively方法只會在signal或signalAll中才會被呼叫),如果沒有則丟擲IllegalMonitorStateException異常。接下來呼叫doSignal方法將等待最久(隊首) 的執行緒從條件佇列轉移到同步佇列

doSignal方法會從目標node(一般都是隊首)開始,將遇到的第一個未被取消的Node從條件佇列移除,並呼叫transferForSignal方法將其放到同步佇列隊尾去等待獲取資源。原始碼如下:

private void doSignal(Node first) {
    do {
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) && (first = firstWaiter) != null);
}

transferForSignal方法不僅會判斷目標node是否被取消,也會將目標node從條件佇列移動到同步佇列。原始碼如下:

final boolean transferForSignal(Node node) {
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))		// 如果CAS失敗,說明node已經被取消了,直接返回false
        return false;

    Node p = enq(node);		// 入隊,返回node的前驅節點p
    int ws = p.waitStatus;
    // 入隊之後希望將前驅節點置為SIGNAL,表明node的執行緒正在苦等獲取資源
    // 如果前驅節點已被取消,或CAS修改前驅的waitStatus失敗,就乾脆直接將node的執行緒喚醒,不多bb
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

總之,signal方法會將最先在同步佇列的(非取消)執行緒移動到同步佇列去排隊獲取資源

signalAll

signalAll方法會將條件佇列中所有的執行緒都移動到同步佇列,讓它們去獲取資源。其原始碼如下:

public final void signalAll() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignalAll(first);
}

該方法會呼叫doSignalAll方法將條件佇列中所有的Node都移除,並放入同步佇列。原始碼如下:

private void doSignalAll(Node first) {
    lastWaiter = firstWaiter = null;		// 將條件佇列清空
    do {
        Node next = first.nextWaiter;
        first.nextWaiter = null;
        transferForSignal(first);
        first = next;
    } while (first != null);
}

總之,signalAll方法會將條件佇列中所有的執行緒都移動到同步佇列去排隊獲取資源
作者:酒冽        出處:https://www.cnblogs.com/frankiedyz/p/15676704.html
版權:本文版權歸作者和部落格園共有
轉載:歡迎轉載,但未經作者同意,必須保留此段宣告;必須在文章中給出原文連線;否則必究法律責任

條件變數的應用

實現生產者-消費者模型

使用上面的非可重入鎖NonReentrantLock的條件變數來實現簡單的生產者-消費者模型(單生產者、單消費者),實現程式碼如下:

public class ProducerConsumerModel {

    private static final NonReentrantLock lock = new NonReentrantLock();
    private static final Condition notFull = lock.newCondition();
    private static final Condition notEmpty = lock.newCondition();

    private static final Queue<String> queue = new LinkedList<>();
    private static final int queueLength = 10;        // 佇列長度

    public static void main(String[] args) {
        Thread producer = new Thread(() -> {
            int i = 0;
            while (true) {
                lock.lock();
                try {
                    while (queue.size() == queueLength) {
                        System.out.println("Queue is full!");
                        notFull.await();
                    }
                    Thread.sleep(2000);
                    System.out.println("Produce product: " + "product" + i);
                    queue.add("product" + i);
                    ++i;
                    notEmpty.signalAll();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        });

        Thread consumer = new Thread(() -> {
            while (true) {
                lock.lock();
                try {
                    while (queue.isEmpty()) {
                        System.out.println("Queue is empty!");
                        notEmpty.await();
                    }
                    Thread.sleep(2000);
                    String product = queue.poll();
                    System.out.println("Consume product: " + product);
                    notFull.signalAll();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        });

        producer.start();
        consumer.start();
    }
}

好了,AQS系列共三期,到此為止,Bye~

全網最詳細的AbstractQueuedSynchronizer(AQS)原始碼剖析(一)AQS基礎
全網最詳細的AbstractQueuedSynchronizer(AQS)原始碼剖析(二)資源的獲取和釋放
全網最詳細的AbstractQueuedSynchronizer(AQS)原始碼剖析(三)條件變數

相關文章