ReentrantLock的條件佇列

李承一發表於2020-12-07

在看這篇文章前推薦看下以下兩篇文章

  1. AbstractQueuedSynchronizer原始碼解讀--續篇之Condition
  2. Java多執行緒之JUC包:Condition原始碼學習筆記

在瞭解Condition的await方法前,推薦先看下signal方法,這個對於後續研究await方法有一定的幫助

除此之外,要了解在Condition中,所有的操作signal、signalAll、await必然都是在已經獲取了鎖的狀態下執行的(例如ReentrantLock),所以很少考慮執行緒安全的問題,這點對分析Condition很有幫助。

1、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指向ConditionQueue的第一個節點,這裡是將原來的節點當成佇列頭
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                // 原來ConditionQueue的頭節點的nexetWaiter設定為null,方便GC
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

預設的signal方法時喚醒等待佇列中的第一個節點,注意是條件佇列,本文稱為ConditionQueue,這裡要和同步佇列SyncQueue區別開,而且兩者最大的區別在於ConditionQueue頭節點把並不是虛結點,在ConditionQueue中節點狀態都為Condition,所以不需要頭節點儲存signal狀態。

 具體來看下transferForSginal()這個方法

    /**
     * Transfers a node from a condition queue onto sync queue.
     * Returns true if successful.
     * @param node the node
     * @return true if successfully transferred (else the node was
     * cancelled before signal)
     */
    final boolean transferForSignal(Node node) {
        /*
         * If cannot change waitStatus, the node has been cancelled.
         */
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        /*
         * 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))
            LockSupport.unpark(node.thread);
        return true;
    }

註釋上寫明瞭,transferForSignal這個方法的作用就是將節點從ConditionQueue轉入到SyncQueue,兩個節點的儲存資料結構都是Node,所以可以互相轉化。

預設情況下ConditionQueue中節點的狀態都condition,這裡第一步是將節點Node轉成初始化節點,也就是狀態為0(初始化)。如果轉化失敗了,說明該節點被取消了。這裡不用考慮是否被其他地方同時設定為0,前面說了當前執行sginal方法的必然是拿到鎖的,所以只能有執行緒自己把狀態設定為cancelled。

把狀態設定為0之後,下面呼叫enq方法將節點入隊,並且設定前面的節點為SIGNAL,讓其釋放鎖是可以成功喚醒自己,之後就可以呼叫LockSupport.unpark將當前執行緒喚醒。

signal方法返回true表示成功將ConditionQueue的頭節點放入到SyncQueue中,失敗則返回false。這時我們回到doSginal方法中。

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

可以看到在while迴圈中,如果transferForSignal如果返回false,就會從ConditionQueue中拿下一個節點直到喚醒成功(下一個節點不為null)。

接下來我們來看await方法

2、await方法

await方法的主要作用就是把當前拿到鎖的節點放入到ConditionQueue中,這點和signal方法有點相反的意思。

        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;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

await方法中,如果呼叫該方法前,該執行緒就已經被中斷了(Thread.currentThread.interrupt()被呼叫),那麼會立即丟擲InterruptEcxception異常。否則,await方法會將當前節點Node加入到ConditionQueue中。

        private Node addConditionWaiter() {
            Node t = lastWaiter;
            // If lastWaiter is cancelled, clean out.
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }

addConditionWaiter方法類似addWaiter,就是將當前執行緒封裝成一個節點,狀態設定為Node.CONDITION,然後放入到ConditionQueue的尾部,不同的是,因為當前執行await的執行緒必然是拿到鎖的,所以它不用考慮到執行緒安全、CAS問題,直接放入到佇列尾部就行,其中ConditionObject(AQS的Condtion實現)用firstWaiter表示ConditionQueue的頭節點,lastWaiter表示ConditionQueue的尾節點。

這裡放入到ConditionQueue之前,會先判斷ConditionQueue最後一個節點的狀態是否為Condition,如果不是就會對ConditionQueue進行整頓。將佇列中的節點狀態為CANCELLED的節點去掉。這裡特意放在await方法中進行處理原因不難想到,當前僅有獲取到鎖的執行緒可以執行await方法,所以放在這裡處理不用考慮複雜的執行緒安全問題。所以這裡unlinkCancelledWaiters進行了簡單的處理。

        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 == null)
                        lastWaiter = trail;
                }
                else
                    trail = t;
                t = next;
            }
        }

unlinkCancelledWaiters的方法不難理解,但是需要提前瞭解以下幾個屬性的意思:

  • firstWaiter:表示ConditionQueue中第一個節點
  • lastWaiter:表示ConditionQueue中最後一個節點狀態
  • t:用來遍歷ConditionQueue中每一個節點
  • next:儲存當前節點的下一個節點資訊,如果當前節點的狀態為CANCELLED的話,就能將當前節點去掉並且接上去
  • trail:儲存重新遍歷中最新的一個狀態為Condition的節點

首先unlinkCancelledWaiters會先拿出ConditionQueue中的第一個節點資訊,如果狀態不為Condition,那麼就一定是CANCELLED,前面說了當前執行await方法的執行緒是拿到鎖的,所以不會有其他執行緒在一次操作ConditionQueue中的節點,注意之裡說的ConditionQueue的節點,不是SyncQueue的節點,SyncQueue的節點在沒有拿到鎖的時候還是可以修改的(例如在acquireQueue中判斷前一個節點狀態為Cancelled時候,會找前面一個狀態為非Cancelled的節點)。所以這裡如果判斷節點狀態為Cancelled,就需要把節點去掉,並且把firstWaiter指向下一個節點。

如果下一個節點狀態為Condition,那麼該節點就需要保留,並且把firstWaiter長期指向於它。所以這裡就有一個問題,如果再下一個節點狀態為Cancelled的時候,如何保障firstWaiter節點的nextWaiter指向下下個節點,例如如下所示

                                                                                         A(CANCELLED)-> B(CONDITION)->C(CANCELLED)->D(CONDITION)->E(CONDITION)->F(CANCELLED)

如果當前指標firstWaiter指向了B,此時C為CANCELLED,就必須把其去掉,讓D接上B的nextWaiter。這裡trail引用就顯得十分重要了。trail會記錄當前從佇列頭到佇列尾最新的一個節點為CONDITION資訊,當發現下一個節點為CANCELLED(例如C),那麼trail(B)就會將nextWaiter指向D,隨後t引用遍歷到D的時候,因為發現D的狀態為Condition,又會把trail記錄為D。

。。。。剩餘await的方法可以參考前面的兩篇部落格,這裡不再闡述~

相關文章