深入淺出AQS之條件佇列

凌風郎少發表於2017-09-30

相比於獨佔鎖跟共享鎖,AbstractQueuedSynchronizer中的條件佇列可能被關注的並不是很多,但它在阻塞佇列的實現裡起著至關重要的作用,同時如果想全面瞭解AQS,條件佇列也是必須要學習的。

原文地址:www.jianshu.com/p/3f8b08ca2…

這篇文章會涉及到AQS中獨佔鎖跟共享鎖的一些知識,如果你已經對這兩塊內容很瞭解了,那就直接往下看。否則在讀本文之前還是建議讀者先去看看我之前寫的兩篇文章溫習一下。
深入淺出AQS之獨佔鎖模式
深入淺出AQS之共享鎖模式

一、使用場景介紹

區別於前面兩篇文章,可能之前很多人都沒有太在意AQS中的這塊內容,所以這篇文章我們先來看下條件佇列的使用場景:

//首先建立一個可重入鎖,它本質是獨佔鎖
private final ReentrantLock takeLock = new ReentrantLock();
//建立該鎖上的條件佇列
private final Condition notEmpty = takeLock.newCondition();
//使用過程
public E take() throws InterruptedException {
        //首先進行加鎖
        takeLock.lockInterruptibly();
        try {
            //如果佇列是空的,則進行等待
            notEmpty.await();
            //取元素的操作...

            //如果有剩餘,則喚醒等待元素的執行緒
            notEmpty.signal();
        } finally {
            //釋放鎖
            takeLock.unlock();
        }
        //取完元素以後喚醒等待放入元素的執行緒
    }複製程式碼

上面的程式碼片段擷取自LinkedBlockingQueue,是Java常用的阻塞佇列之一。
從上面的程式碼可以看出,條件佇列是建立在鎖基礎上的,而且必須是獨佔鎖(原因後面會通過原始碼分析)。

二、執行過程概述

等待條件的過程:

  1. 在操作條件佇列之前首先需要成功獲取獨佔鎖,不然直接在獲取獨佔鎖的時候已經被掛起了。
  2. 成功獲取獨佔鎖以後,如果當前條件還不滿足,則在當前鎖的條件佇列上掛起,與此同時釋放掉當前獲取的鎖資源。這裡可以考慮一下如果不釋放鎖資源會發生什麼?
  3. 如果被喚醒,則檢查是否可以獲取獨佔鎖,否則繼續掛起。

條件滿足後的喚醒過程(以喚醒一個節點為例,也可以喚醒多個):

  1. 把當前等待佇列中的第一個有效節點(如果被取消就無效了)加入同步佇列等待被前置節點喚醒,如果此時前置節點被取消,則直接喚醒該節點讓它重新在同步佇列裡適當的嘗試獲取鎖或者掛起。

注:說到這裡必須要解釋一個知識點,整個AQS分為兩個佇列,一個同步佇列,一個條件佇列。只有同步佇列中的節點才能獲取鎖。前面兩篇獨佔鎖共享鎖文章中提到的加入佇列就是同步佇列。條件佇列中所謂的喚醒是把節點從條件佇列移到同步佇列,讓節點有機會去獲取鎖。

二、原始碼深入分析

下面的程式碼稍微複雜一點,因為它考慮了中斷的處理情況。我由於想跟文章開頭的程式碼片段保持一致,所以選取了該方法進行說明。如果只想看核心邏輯的話,那推薦讀者看看awaitUninterruptibly()方法的原始碼。

        //條件佇列入口,參考上面的程式碼片段
        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) 
                unlinkCancelledWaiters();
            //根據不同模式處理中斷
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }複製程式碼

流程比較複雜,一步一步來分析,首先看下加入條件佇列的程式碼:

        //注:1.與同步佇列不同,條件佇列頭尾指標是firstWaiter跟lastWaiter
        //注:2.條件佇列是在獲取鎖之後,也就是臨界區進行操作,因此很多地方不用考慮併發
        private Node addConditionWaiter() {
            Node t = lastWaiter;
            //如果最後一個節點被取消,則刪除佇列中被取消的節點
            //至於為啥是最後一個節點後面會分析
            if (t != null && t.waitStatus != Node.CONDITION) {
                //刪除所有被取消的節點
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
            //建立一個型別為CONDITION的節點並加入佇列,由於在臨界區,所以這裡不用併發控制
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return 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 == null)
                        lastWaiter = trail;
                }
                else
                    trail = t;
                t = next;
            }
        }複製程式碼

把節點加入到條件佇列中以後,接下來要做的就是釋放鎖資源:

    //入參就是新建立的節點,即當前節點
    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;
        }
    }複製程式碼

走到這一步,節點也加入條件佇列中了,鎖資源也釋放了,接下來就該掛起了(先忽略中斷處理,單看掛起邏輯):

     //如果不在同步佇列就繼續掛起(signal操作會把節點加入同步佇列)
     while (!isOnSyncQueue(node)) {
           LockSupport.park(this);
           //中斷處理後面再分析
           if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                break;
     }
    //判斷節點是否在同步佇列中
    final boolean isOnSyncQueue(Node node) {
        //快速判斷1:節點狀態或者節點沒有前置節點
        //注:同步佇列是有頭節點的,而條件佇列沒有
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        //快速判斷2:next欄位只有同步佇列才會使用,條件佇列中使用的是nextWaiter欄位
        if (node.next != null) 
            return true;
        //上面如果無法判斷則進入複雜判斷
        return findNodeFromTail(node);
    }

    //注意這裡用的是tail,這是因為條件佇列中的節點是被加入到同步佇列尾部,這樣查詢更快
    //從同步佇列尾節點開始向前查詢當前節點,如果找到則說明在,否則不在
    private boolean findNodeFromTail(Node node) {
        Node t = tail;
        for (;;) {
            if (t == node)
                return true;
            if (t == null)
                return false;
            t = t.prev;
        }
    }複製程式碼

如果被喚醒且已經被轉移到了同步佇列,則會執行與獨佔鎖一樣的方法acquireQueued()進行同步佇列獨佔獲取。
最後我們來梳理一下里面的中斷邏輯以及收尾工作的程式碼:

     while (!isOnSyncQueue(node)) {
           LockSupport.park(this);
           //這裡被喚醒可能是正常的signal操作也可能是中斷
           if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                break;
     }

     //這裡的判斷邏輯是:
     //1.如果現在不是中斷的,即正常被signal喚醒則返回0
     //2.如果節點由中斷加入同步佇列則返回THROW_IE,由signal加入同步佇列則返回REINTERRUPT
     private int checkInterruptWhileWaiting(Node node) {
           return Thread.interrupted() ?
                (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
                0;
     }

     //修改節點狀態並加入同步佇列
     //該方法返回true表示節點由中斷加入同步佇列,返回false表示由signal加入同步佇列
     final boolean transferAfterCancelledWait(Node node) {
        //這裡設定節點狀態為0,如果成功則加入同步佇列
        if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
            //與獨佔鎖同樣的加入佇列邏輯,不贅述
            enq(node);
            return true;
        }
        //如果上面設定失敗,說明節點已經被signal喚醒,由於signal操作會將節點加入同步佇列,我們只需自旋等待即可
        while (!isOnSyncQueue(node))
            Thread.yield();
        return false;
     }複製程式碼

在把喚醒後的中斷判斷做好以後,看await()中最後一段邏輯:

//在處理中斷之前首先要做的是從同步佇列中成功獲取鎖資源
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
      interruptMode = REINTERRUPT;
//由於當前節點可能是由於中斷修改了節點狀態,所以如果有後繼節點則執行刪除已取消節點的操作
//如果沒有後繼節點,根據上面的分析在後繼節點加入的時候會進行刪除
if (node.nextWaiter != null) 
      unlinkCancelledWaiters();
if (interruptMode != 0)
      reportInterruptAfterWait(interruptMode);

//根據中斷時機選擇丟擲異常或者設定執行緒中斷狀態
private void reportInterruptAfterWait(int interruptMode) throws InterruptedException {
      if (interruptMode == THROW_IE)
           throw new InterruptedException();
      else if (interruptMode == REINTERRUPT)
           //實現程式碼為:Thread.currentThread().interrupt();
           selfInterrupt();
}複製程式碼

至此條件佇列await操作全部分析完畢。signal()方法相對容易一些,一起看原始碼分析下:

   //條件佇列喚醒入口
   public final void signal() {
       //如果不是獨佔鎖則丟擲異常,再次說明條件佇列只適用於獨佔鎖
       if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
       //如果條件佇列不為空,則進行喚醒操作
       Node first = firstWaiter;
       if (first != null)
            doSignal(first);
   }

   //該方法就是把一個有效節點從條件佇列中刪除並加入同步佇列
   //如果失敗則會查詢條件佇列上等待的下一個節點直到佇列為空
   private void doSignal(Node first) {
        do {
            if ( (firstWaiter = first.nextWaiter) == null)
                lastWaiter = null;
            first.nextWaiter = null;
        } while (!transferForSignal(first) &&(first = firstWaiter) != null);
   }

    //將節點加入同步佇列
    final boolean transferForSignal(Node node) {
        //修改節點狀態,這裡如果修改失敗只有一種可能就是該節點被取消,具體看上面await過程分析
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
        //該方法很熟悉了,跟獨佔鎖入隊方法一樣,不贅述
        Node p = enq(node);
        //注:這裡的p節點是當前節點的前置節點
        int ws = p.waitStatus;
        //如果前置節點被取消或者修改狀態失敗則直接喚醒當前節點
        //此時當前節點已經處於同步佇列中,喚醒會進行鎖獲取或者正確的掛起操作
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }複製程式碼

三、總結

相比於獨佔鎖跟共享鎖,條件佇列可能是最不受關注的了,但由於它是阻塞佇列實現的關鍵元件,還是有必要了解一下其中的原理。其實我認為關鍵點有兩條,第一是條件佇列是建立在某個具體的鎖上面的,第二是條件佇列跟同步佇列是兩個佇列,前者依賴條件喚醒後者依賴鎖釋放喚醒,瞭解了這兩點以後搞清楚條件佇列就不是什麼難事了。


至此,Java同步器AQS中三大鎖模式就都分析完了。雖然已經盡力思考,儘量寫的清楚,但鑑於水平有限,如果有紕漏的地方,歡迎廣大讀者指正。
明天就是國慶長假了,我自己也計劃出國玩一趟,散散心。
提前祝廣大朋友國慶快樂。

相關文章