Java併發包原始碼學習系列:詳解Condition條件佇列、signal和await

天喬巴夏丶發表於2021-01-17

系列傳送門:

Condition介面

Contition是一種廣義上的條件佇列,它利用await()和signal()為執行緒提供了一種更為靈活的等待/通知模式

圖源:《Java併發程式設計的藝術》

Condition必須要配合Lock一起使用,因為對共享狀態變數的訪問發生在多執行緒環境下。

一個Condition的例項必須與一個Lock繫結,因此await和signal的呼叫必須在lock和unlock之間有鎖之後,才能使用condition嘛。以ReentrantLock為例,簡單使用如下:

public class ConditionTest {

    public static void main(String[] args) {
        final ReentrantLock lock = new ReentrantLock();
        final Condition condition = lock.newCondition();

        Thread thread1 = new Thread(() -> {
            String name = Thread.currentThread().getName();

            lock.lock();
            System.out.println(name + " <==成功獲取到鎖" + lock);
            try {
                System.out.println(name + " <==進入條件佇列等待");
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(name + " <==醒了");
            lock.unlock();
            System.out.println(name + " <==釋放鎖");
        }, "等待執行緒");

        thread1.start();

        Thread thread2 = new Thread(() -> {
            String name = Thread.currentThread().getName();

            lock.lock();
            System.out.println(name + " ==>成功獲取到鎖" + lock);
            try {
                System.out.println("========== 這裡演示await中的執行緒沒有被signal的時候會一直等著 ===========");
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(name + " ==>通知等待佇列的執行緒");
            condition.signal();
            lock.unlock();
            System.out.println(name + " ==>釋放鎖");
        }, "通知執行緒");

        thread2.start();
    }
}
等待執行緒 <==成功獲取到鎖java.util.concurrent.locks.ReentrantLock@3642cea8[Locked by thread 等待執行緒]
等待執行緒 <==進入條件佇列等待
通知執行緒 ==>成功獲取到鎖java.util.concurrent.locks.ReentrantLock@3642cea8[Locked by thread 通知執行緒]
========== 這裡演示await中的執行緒沒有被signal的時候會一直等著 ===========
通知執行緒 ==>通知等待佇列的執行緒
通知執行緒 ==>釋放鎖
等待執行緒 <==醒了
等待執行緒  <==釋放鎖

接下來我們將從原始碼的角度分析上面這個流程,理解所謂條件佇列的內涵。

AQS條件變數的支援之ConditionObject內部類

AQS,Lock,Condition,ConditionObject之間的關係:

ConditionObject是AQS的內部類,實現了Condition介面,Lock中提供newCondition()方法,委託給內部AQS的實現Sync來建立ConditionObject物件,享受AQS對Condition的支援。

    // ReentrantLock#newCondition
	public Condition newCondition() {
        return sync.newCondition();
    }
	// Sync#newCondition
    final ConditionObject newCondition() {
        // 返回Contition的實現,定義在AQS中
        return new ConditionObject();
    }

ConditionObject用來結合鎖實現執行緒同步,ConditionObject可以直接訪問AQS物件內部的變數,比如state狀態值和AQS佇列

ConditionObject是條件變數,每個條件變數對應一個條件佇列(單向連結串列佇列),其用來存放呼叫條件變數的await方法後被阻塞的執行緒,ConditionObject維護了首尾節點,沒錯這裡的Node就是我們之前在學習AQS的時候見到的那個Node,我們會在下面回顧:

public class ConditionObject implements Condition, java.io.Serializable {
    private static final long serialVersionUID = 1173984872572414699L;
    /** 條件佇列的第一個節點. */
    private transient Node firstWaiter;
    /** 條件佇列的最後一個節點. */
    private transient Node lastWaiter;
}

看到這裡我們需要明確這裡的條件佇列和我們之前說的AQS同步佇列是不一樣的:

  • AQS維護的是當前在等待資源的佇列,Condition維護的是在等待signal訊號的佇列。
  • 每個執行緒會存在上述兩個佇列中的一個,lock與unlock對應在AQS佇列,signal與await對應條件佇列,執行緒節點在他們之間反覆橫跳。

這裡著重說明一下,接下來的原始碼學習部分,我們會將兩個佇列進行區分,涉及到同步佇列和阻塞佇列的描述,意味著是AQS的同步佇列,而條件佇列指的是Condition佇列,望讀者知曉。

這裡我們針對上面的demo來分析一下會更好理解一些:

為了簡化,接下來我將用D表示等待執行緒,用T表示通知執行緒

  1. 【D】先呼叫lock.lock()方法,此時無競爭,【D】被加入到AQS同步佇列中。
  2. 【D】呼叫condition.await()方法,此時【D】被構建為等待節點並加入到condition對應的條件等待佇列中,並從AQS同步佇列中移除。
  3. 【D】陷入等待之後,【T】啟動,由於AQS佇列中的【D】已經被移除,此時【T】也很快獲取到鎖,相應的,【T】也被加入到AQS同步佇列中。
  4. 【T】接著呼叫condition.signal()方法,這時condition對應的條件佇列中只有一個節點【D】,於是【D】被取出,並被再次加入AQS的等待佇列中。此時【D】並沒有被喚醒,只是單純換了個位置。
  5. 接著【T】執行lock.unlock(),釋放鎖鎖之後,會喚醒AQS佇列中的【D】,此時【D】真正被喚醒且執行。

OK,lock -> await -> signal -> unlock這一套流程相信已經大概能夠理解,接下來我們試著看看原始碼吧。

回顧AQS中的Node

我們這裡再簡單回顧一下AQS中Node類與Condition相關的欄位:

        // 記錄當前執行緒的等待狀態,
        volatile int waitStatus;

        // 前驅節點
        volatile Node prev;

        // 後繼節點
        volatile Node next;

        // node儲存的執行緒
        volatile Thread thread;
		
        // 當前節點在Condition中等待佇列上的下一個節點
        Node nextWaiter;

waitStatus可以取五種狀態:

  1. 初始化為0,啥也不表示,之後會被置signal。
  2. 1表示cancelled,取消當前執行緒對鎖的爭奪。
  3. -1表示signal,表示當前節點釋放鎖後需要喚醒後面可被喚醒的節點。
  4. -2表示condition,我們這篇的重點,表示當前節點在條件佇列中
  5. -3表示propagate,表示釋放共享資源的時候會向後傳播釋放其他共享節點。

當然,除了-2這個condition狀態,其他的等待狀態我們之前都或多或少分析過,今天著重學習condition這個狀態的意義。

我們還可以看到一個Node型別的nextWaiter,它表示條件佇列中當前節點的下一個節點,可以看出用以實現條件佇列的單向連結串列。

void await()

呼叫Condition的await()方法,會使當前執行緒進入等待佇列並釋放鎖,同時執行緒狀態變為等待狀態。

其實就是從AQS同步佇列的首節點,注意不是head,而是獲取了鎖的節點,移動到Condition的等待佇列中。

瞭解這些之後,我們直接來看看具體方法的原始碼:

        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;
            }
            // 上面的迴圈退出有兩種情況:
            // 1. isOnSyncQueue(node) 為true,即當前的node已經轉移到阻塞佇列了
            // 2. checkInterruptWhileWaiting != 0, 表示執行緒中斷
            
            // 退出迴圈,被喚醒之後,進入阻塞佇列,等待獲取鎖 acquireQueued
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

新增到條件佇列

Node addConditionWaiter()

addConditionWaiter() 是將當前節點加入到條件佇列中:

        private Node addConditionWaiter() {
            Node t = lastWaiter;
            // 如果lastWaiter被取消了,將其清除
            if (t != null && t.waitStatus != Node.CONDITION) {
                // 遍歷整個條件佇列,將已取消的所有節點清除出列
                unlinkCancelledWaiters();
           		// t重新賦值一下,因為last可能改變了
                t = lastWaiter;
            }
            //注意這裡,node在初始化的時候,會指定ws為CONDITION
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            // t == null 表示佇列此時為空,初始化firstWaiter
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;// 入隊尾
            lastWaiter = node;// 將尾指標指向新建的node
            return node;
        }

void unlinkCancelledWaiters()

unlinkCancelledWaiters用於清除佇列中已經取消等待的節點。

        
		private void unlinkCancelledWaiters() {
            Node t = firstWaiter;
            // trail這裡表示取消節點的前驅節點
            Node trail = null;
            // t會從頭到尾遍歷這個單連結串列
            while (t != null) {
                // next用於儲存下一個
                Node next = t.nextWaiter;
                // 如果發現當前這個節點 不是 condition了, 那麼考慮移除它
                // 下面是單連結串列的移除節點操作 簡單來說就是 trail.next = t.next
                if (t.waitStatus != Node.CONDITION) {
                    t.nextWaiter = null;
                    // 說明first就是不是condition了
                    if (trail == null)
                        firstWaiter = next;
                    else
                        //trail.next = t.next
                        trail.nextWaiter = next;
                    // trail後面沒東西,自然trail就是lastWaiter了
                    if (next == null)
                        lastWaiter = trail;
                }
                // 當前節點是一直跟到不是condition節點的上一個
                else
                    trail = t;
                // 向後遍歷 t = t.next
                t = next;
            }
        }

總結一下addConditionWaiter的過程:

  1. 首先判斷條件佇列的尾節點是否被取消了,這裡用last.ws != CONDITION來判斷,如果是的話,就需要從頭到尾遍歷,消除被不是condition的節點。
  2. 接著將當前執行緒包裝為Node,指定ws為CONDITION。

完全釋放獨佔鎖

將節點加入等待佇列中後,就需要完全釋放執行緒擁有的獨佔鎖了,完全釋放針對重入鎖的情況。我們可以拉到await()方法中看看,將會呼叫:int savedState = fullyRelease(node);,這句話有什麼內涵呢?

我們看到這個方法返回了一個savedState變數,簡單的理解就是儲存狀態。我們知道重入鎖的state由重入的次數,如果一個state為N,我們可以認為它持有N把鎖。

await()方法必須將state置0,也就是完全釋放鎖,後面的執行緒才能獲取到這把鎖,置0之後,我們需要用個變數標記一下,也就是這裡的savedState。

這樣它被重新喚醒的時候,我們就知道,他需要獲取savedState把鎖。

int fullyRelease(Node node)

    final int fullyRelease(Node node) {
        boolean failed = true;
        try {
            // 獲取當前的state值,重入次數
            int savedState = getState();
            // 釋放N = savedState資源
            if (release(savedState)) {
                failed = false;
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
            // 如果獲取失敗,將會將節點設定為取消狀態,並丟擲異常
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }

這裡其實我們就會明白開頭說的:如果某個執行緒沒有獲取lock,就直接呼叫condition的await()方法,結果是什麼呢,在release的時候丟擲異常,然後節點被取消,之後節點進來的時候,將它清理掉。

等待進入阻塞佇列

ok,完全釋放鎖之後,將會來到這幾步,如果這個節點的執行緒不在同步佇列中,說明該執行緒還不具備競爭鎖的資格,將被一直掛起,這裡的同步佇列指的是AQS的阻塞佇列。

            int interruptMode = 0;
            // 如果這個節點的執行緒不在同步佇列中,說明該執行緒還不具備競爭鎖的資格,會一直掛起
            while (!isOnSyncQueue(node)) {
                // 掛起執行緒
                LockSupport.park(this);
                // 如果執行緒中斷,退出
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }

boolean isOnSyncQueue(Node node)

下面這個方法會判斷節點是不是已經到阻塞佇列中了,如果是的話,就直接返回true,這個方法的必要性是什麼呢?

其實啊,這裡需要提前說一下signal()方法,signal的作用和await()方法,將在等待佇列中阻塞的節點移動到AQS同步佇列中,這個方法就是說判斷一下這個節點是不是移過去了。

    final boolean isOnSyncQueue(Node node) {
        // 1. 節點的等待狀態還是condition表示還在等待佇列中
        // 2. node.prev == null 表示還沒移到阻塞佇列中[prev和next都是阻塞佇列中用的]

        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
     
        // 如果node已經有了後繼節點,表示已經在阻塞佇列中了
        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.
         */
        // 來到這裡的情況:ws != condition && node.prev != null && node.next == null
        
		// 想想:為什麼node.prev != null不能作為判斷不在阻塞佇列的依據呢?
        // CAS首先設定node.prev 指向tail,這個時候node.prev 是不為null的,但CAS可能會失敗
        return findNodeFromTail(node);
    }

為什麼node.prev != null不能作為判斷不在阻塞佇列的依據呢?

官方給出瞭解答: 因為CAS的入隊操作中,首先設定node.prev 指向tail,這個時候node.prev 是不為null的。你能夠說他入隊成功一定成功嗎?不一定,因為CAS可能會失敗,所以要findNodeFromTail(node)。

boolean findNodeFromTail(Node node)

從阻塞佇列的尾部向前遍歷,如果找到這個node,表示它已經在了,那就返回true。

    private boolean findNodeFromTail(Node node) {
        Node t = tail;
        for (;;) {
            // 已經有了
            if (t == node)
                return true;
            // 尾都沒有,找啥呢,返回false
            if (t == null)
                return false;
            // 一直往前找
            t = t.prev;
        }
    }

void signal()

由於之前節點被加入等待佇列將會一直阻塞,為了連貫性,我們來看看喚醒它的signal方法吧:

之前說到,如果這個執行緒會在等待佇列中等待,那麼喚醒它的signal方法的流程是怎麼樣的呢,前面其實已經說了一丟丟了,我們猜測,signal會將isOnSyncQueue方法的迴圈打破,接下來看看是不是這樣子的。

        public final void signal() {
            // 一樣的,必須佔有當前這個鎖才能用signal方法
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }

喚醒節點

該方法會從頭到尾遍歷條件佇列,找到需要移到同步佇列的節點。

void doSignal(Node first)

        private void doSignal(Node first) {
            do {
                // firstWaiter 指向first的下一個
                if ( (firstWaiter = first.nextWaiter) == null)
                    // 如果first是最後一個且要被移除了,就將last置null
                    lastWaiter = null;
                // first斷絕與條件佇列的連線
                first.nextWaiter = null;
                // fisrt轉移失敗,就看看後面是不是需要的
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

這裡的while迴圈表示,如果first沒有轉移成功,就接著判斷first後面的節點是不是需要轉移。

boolean transferForSignal(Node node)

該方法將節點從條件佇列轉移到阻塞佇列。

    final boolean transferForSignal(Node node) {
        /*
         * CAS操作嘗試將Condition的節點的ws改為0
         * 如果失敗,意味著:節點的ws已經不是CONDITION,說明節點已經被取消了
         * 如果成功,則該節點的狀態ws被改為0了
         */
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        /*
         * 通過enq方法將node自旋的方式加入同步佇列隊尾
         * 這裡放回的p是node在同步佇列的前驅節點
         */
        Node p = enq(node);
        int ws = p.waitStatus;
        // ws大於0 的情況只有 cancenlled,表示node的前驅節點取消了爭取鎖,那直接喚醒node執行緒
        // ws <= 0 會使用cas操作將前驅節點的ws置為signal,如果cas失敗也會喚醒node
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }
	// 自旋的方式入隊
    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;
                    // 返回的是node的前驅節點
                    return t;
                }
            }
        }
    }

檢查中斷狀態

ok,一旦signal之後,節點被成功轉移到同步佇列後,這時下面這個迴圈就會退出了,繼續回到這裡:

            int interruptMode = 0;
            // 如果這個節點的執行緒不在同步佇列中,說明該執行緒還不具備競爭鎖的資格,會一直掛起
            while (!isOnSyncQueue(node)) {
                // 掛起執行緒
                LockSupport.park(this);
                // 如果執行緒中斷,退出
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }

interruptMode可以有以下幾種取值:

        /** await 返回的時候,需要重新設定中斷狀態 */
        private static final int REINTERRUPT =  1;
        /** await 返回的時候,需要丟擲 InterruptedException 異常 */
        private static final int THROW_IE    = -1;
		
		/** interruptMode取0的時候表示在await()期間,沒有發生中斷 */

說到這裡我們需要明白,LockSupport.park(this)掛起的執行緒是什麼時候喚醒的:

  1. signal方法將節點轉移到同步佇列中,且獲取到了鎖或者對前驅節點的cas操作失敗,呼叫了LockSupport.unpark(node.thread);方法。
  2. 在park的時候,另外一個執行緒對掛起的執行緒進行了中斷。

喚醒之後,我們可以看到呼叫checkInterruptWhileWaiting方法檢查等待期間是否發生了中斷,如果不為0表示確實在等待期間發生了中斷。

但其實這個方法的返回結果用interruptMode變數接收,擁有更加豐富的內涵,它還能夠判斷中斷的時機是否在signal之前。

int checkInterruptWhileWaiting(Node node)

該方法用於判斷該執行緒是否在掛起期間發生了中斷。

        private int checkInterruptWhileWaiting(Node node) {
            return Thread.interrupted() ?// 如果處於中斷狀態,返回true,且將重置中斷狀態
                (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :// 如果中斷了,判斷何時中斷
                0; // 沒有中斷, 返回0
        }

boolean transferAfterCancelledWait(Node node)

該方法判斷何時中斷,是否在signal之前。

    final boolean transferAfterCancelledWait(Node node) {
        // 嘗試使用CAS操作將node 的ws設定為0
        // 如果成功,說明在signal方法之前中斷就已經發生:
        // 原因在於:signal如果在此之前發生,必然已經cas操作將ws設定為0了,這裡不可能設定成功
        if (compareAndSetWaitStatus(node, Node.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之後發生的中斷
         * 但是signal可能還在進行轉移中,這邊自旋等一下它完成
         */
        while (!isOnSyncQueue(node))
            Thread.yield();
        return false;
    }

這裡的話,我們還是稍微總結一下:

  1. await()中的節點中斷之後,被喚醒有多種情況:
    • 無中斷的情況:signal方法成功將節點移入同步佇列且節點成功獲取資源,喚醒該執行緒,此時退出的時候interruptMode為0。
    • 有中斷的情況:
      • signal之前中斷,interruptMode設定為THROW_IE。
      • signal之後中斷,interruptMode設定為REINTERRUPT。
  2. 中斷時,無論signal之前或之後,節點無論如何都會進入阻塞佇列。

處理中斷狀態

接下來三個部分我將一一說明:

            // 第一部分
			if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
			// 第二部分
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters(); // 清除取消的節點
			// 第三部分
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);

第一部分

signal喚醒的執行緒並不會立即獲取到資源,從while迴圈退出後,會通過acquireQueued方法加入獲取同步狀態的競爭中。

            // 第一部分
			if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;

acquireQueued(node, savedState)中node此時已經被加入同步佇列了,savedState是之前儲存的state。

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted; // 
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

acquireQueued方法返回時,表示已經獲取到了鎖,且返回的是interrupted值,如果返回true,表示已經被中斷。

接著判斷interruptMode != THROW_IE表示是在signal之後發生的中斷,需要重新中斷當前執行緒,將interruptMode設定為REINTERRUPT。

第二部分

			// 第二部分
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters(); // 清除取消的節點

前面說了,signal會將節點移到同步佇列中,最後一步需要和條件佇列斷開關係,也就是:node.nextWaiter = null,但這是想象中比較正常的情況,如果在signal之前被中斷,節點也會被加入同步佇列中,這時其實是沒有呼叫這個斷開關係的。

因此這邊做一點處理, unlinkCancelledWaiters()邏輯上面也說過了,可以回過頭去看看,主要是清除佇列中已經取消等待的節點。

第三部分

最後一個部分,就是對兩種interruptMode的情況進行處理,看看程式碼就知道了:

void reportInterruptAfterWait(interruptMode)

        private void reportInterruptAfterWait(int interruptMode)
            throws InterruptedException {
            // signal 之前的中斷, 需要丟擲異常
            if (interruptMode == THROW_IE)
                throw new InterruptedException();
            // signal 之後發生的中斷, 需要重新中斷
            else if (interruptMode == REINTERRUPT)
                selfInterrupt();
        }

帶超機制的void await()

帶超時機制的await()方法有以下幾個,簡單看下即可:

  • long awaitNanos(long nanosTimeout)
  • boolean awaitUntil(Date deadline)
  • boolean await(long time, TimeUnit unit)

我們選最後一個來看看,主要看看和之前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) {
                    // 這個方法返回true表示在這個方法內,已經將節點轉移到阻塞佇列中
                    // 返回false,表示signal已經發生,表示沒有超時
                    timedout = transferAfterCancelledWait(node);
                    break;
                }
                //spinForTimeoutThreshold 是AQS中的一個欄位,如果超過1ms,使用parkNonos
                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;
        }

不丟擲InterruptedException的await

        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;
            }
            // 相比await() 針對中斷少了丟擲異常的操作,而是直接進行中斷
            if (acquireQueued(node, savedState) || interrupted)
                selfInterrupt();
        }

Condition的使用

最後以一個Java doc給的例子結尾吧:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

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 {
            while (count == items.length)
                notFull.await();  // 佇列已滿,等待,直到 not full 才能繼續生產
            items[putptr] = x;
            if (++putptr == items.length) putptr = 0;
            ++count;
            notEmpty.signal(); // 生產成功,佇列已經 not empty 了,發個通知出去
        } finally {
            lock.unlock();
        }
    }

    // 消費
    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0)
                notEmpty.await(); // 佇列為空,等待,直到佇列 not empty,才能繼續消費
            Object x = items[takeptr];
            if (++takeptr == items.length) takeptr = 0;
            --count;
            notFull.signal(); // 被我消費掉一個,佇列 not full 了,發個通知出去
            return x;
        } finally {
            lock.unlock();
        }
    }
}

其實這個之前也說過,ArrayBlockingQueue就是採用了這種方式實現的生產者-消費者模式,如果你感興趣,可以看看具體的實現細節哦。

總結

  • Condition的await()和signal()基於Lock,相比於基於Object的wait()和notify()方法,它提供更加靈活的等待通知的機制。
  • 支援豐富的功能如:帶超時機制的await(),不響應中斷的await(),以及多個等待的條件佇列。
  • Condition的await()方法會將執行緒包裝為等待節點,加入等待佇列中,並將AQS同步佇列中的節點移除,接著不斷檢查isOnSyncQueue(Node node),如果在等待佇列中,就一直等著,如果signal將它移到AQS佇列中,則退出迴圈。
  • Condition的signal()方法則是先檢查當前執行緒是否獲取了鎖,接著將等待佇列中的節點通過Node的操作直接加入AQS佇列。執行緒並不會立即獲取到資源,從while迴圈退出後,會通過acquireQueued方法加入獲取同步狀態的競爭中。

參考閱讀

相關文章