AQS原始碼深入分析之條件佇列-你知道Java中的阻塞佇列是如何實現的嗎?

雕爺的架構之路發表於2020-11-09

本文基於JDK-8u261原始碼分析


1 簡介

img

因為CLH佇列中的執行緒,什麼執行緒獲取到鎖,什麼執行緒進入佇列排隊,什麼執行緒釋放鎖,這些都是不受我們控制的。所以條件佇列的出現為我們提供了主動式地、只有滿足指定的條件後才能執行緒阻塞和喚醒的方式。對於條件佇列首先需要說明一些概念:條件佇列是AQS中除了CLH佇列之外的另一種佇列,每建立一個Condition實際上就是建立了一個條件佇列,而每呼叫一次await方法實際上就是往條件佇列中入隊,每呼叫一次signal方法實際上就是往條件佇列中出隊。不像CLH佇列上節點的狀態有多個,條件佇列上節點的狀態只有一個:CONDITION。所以如果條件佇列上一個節點不再是CONDITION狀態時,就意味著這個節點該出隊了。需要注意的是,條件佇列只能執行在獨佔模式下

一般在使用條件佇列作為阻塞佇列來使用時都會建立兩個條件佇列:notFullnotEmpty。notFull表示當條件佇列已滿的時候,put方法會處於等待狀態,直到佇列沒滿;notEmpty表示當條件佇列為空的時候,take方法會處於等待狀態,直到佇列有資料了。

而notFull.signal方法和notEmpty.signal方法會將條件佇列上的節點移到CLH佇列中(每次只轉移一個)。也就是說,存在一個節點從條件佇列被轉移到CLH佇列的情況發生。同時也意味著,條件佇列上不會發生鎖資源競爭,所有的鎖競爭都是發生在CLH佇列上的

其他一些條件佇列和CLH佇列之間的差異如下:

  • 條件佇列使用nextWaiter指標來指向下一個節點,是一個單向連結串列結構,不同於CLH佇列的雙向連結串列結構;
  • 條件佇列使用firstWaiter和lastWaiter來指向頭尾指標,不同於CLH佇列的head和tail;
  • 條件佇列中的第一個節點也不會像CLH佇列一樣,是一個特殊的空節點;
  • 不同於CLH佇列中會用很多的CAS操作來控制併發,條件佇列進佇列的前提是已經獲取到了獨佔鎖資源,所以很多地方不需要考慮併發。

下面就是具體的原始碼分析了。條件佇列以ArrayBlockingQueue來舉例:


2 構造器

 1  /**
 2   * ArrayBlockingQueue:
 3   */
 4  public ArrayBlockingQueue(int capacity) {
 5    this(capacity, false);
 6}
 7
 8  public ArrayBlockingQueue(int capacity, boolean fair) {
 9    if (capacity <= 0)
10        throw new IllegalArgumentException();
11    //存放實際資料的陣列
12    this.items = new Object[capacity];
13    //獨佔鎖使用ReentrantLock來實現(fair表示的就是公平鎖還是非公平鎖,預設為非公平鎖)
14    lock = new ReentrantLock(fair);
15    //notEmpty條件佇列
16    notEmpty = lock.newCondition();
17    //notFull條件佇列
18    notFull = lock.newCondition();
19  }

3 put方法

  1  /**
  2   * ArrayBlockingQueue:
  3   */
  4  public void put(E e) throws InterruptedException {
  5    //非空校驗
  6    checkNotNull(e);
  7    final ReentrantLock lock = this.lock;
  8    /*
  9    獲取獨佔鎖資源,響應中斷模式。其實現程式碼和lock方法還有Semaphore的acquire方法是類似的
 10    因為這裡分析的是條件佇列,於是就不再分析該方法的細節了
 11     */
 12    lock.lockInterruptibly();
 13    try {
 14        while (count == items.length)
 15            //如果陣列中資料已經滿了的話,就在notFull中入隊一個新節點,並阻塞當前執行緒
 16            notFull.await();
 17        //新增陣列元素並喚醒notEmpty
 18        enqueue(e);
 19    } finally {
 20        //釋放鎖資源
 21        lock.unlock();
 22    }
 23  }

4 await方法

如果在put的時候發現陣列已滿,或者在take的時候發現陣列是空的,就會呼叫await方法來將當前節點放入條件佇列中:

 1  /**
 2   * AbstractQueuedSynchronizer:
 3   */
 4  public final void await() throws InterruptedException {
 5    //如果當前執行緒被中斷就丟擲異常
 6    if (Thread.interrupted())
 7        throw new InterruptedException();
 8    //把當前節點加入到條件佇列中
 9    Node node = addConditionWaiter();
10    //釋放之前獲取到的鎖資源,因為後續會阻塞該執行緒,所以如果不釋放的話,其他執行緒將會等待該執行緒被喚醒
11    int savedState = fullyRelease(node);
12    int interruptMode = 0;
13    //如果當前節點不在CLH佇列中則阻塞住,等待unpark喚醒
14    while (!isOnSyncQueue(node)) {
15        LockSupport.park(this);
16        /*
17        這裡被喚醒可能是正常的signal操作也可能是被中斷了。但無論是哪種情況,都會將當前節點插入到CLH佇列尾,
18        並退出迴圈(注意,這裡被喚醒除了上面兩種情況之外,還有一種情況是作業系統級別的虛假喚醒(spurious wakeup),
19        也就是當前執行緒毫無理由就會被喚醒了,所以上面需要使用while來規避掉這種情況)
20         */
21        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
22            break;
23    }
24    //走到這裡說明當前節點已經插入到了CLH佇列中(被signal所喚醒或者被中斷)。然後在CLH佇列中進行獲取鎖資源的操作
25    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
26        /*
27        <<<THROW_IE和REINTERRUPT的解釋詳見transferAfterCancelledWait方法>>>
28
29        之前分析過的如果acquireQueued方法返回true,說明當前執行緒被中斷了
30        返回true意味著在acquireQueued方法中此時會再一次被中斷(注意,這意味著有兩個程式碼點判斷執行緒是否被中斷:
31        一個是在第15行程式碼處,另一個是在acquireQueued方法裡面),如果之前沒有被中斷,則interruptMode=0,
32        而在acquireQueued方法裡面執行緒被中斷返回了,這個時候將interruptMode重新修正為REINTERRUPT即可
33        至於為什麼不修正為THROW_IE是因為在這種情況下,第15行程式碼處已經通過呼叫signal方法正常喚醒了,
34        節點已經放進了CLH佇列中。而此時的中斷是在signal操作之後,在第25行程式碼處去搶鎖資源的時候發生的
35        這個時候中斷不中斷已經無所謂了,所以就不需要丟擲InterruptedException
36         */
37        interruptMode = REINTERRUPT;
38    /*
39    走到這裡說明當前節點已經獲取到了鎖資源(獲取不到的話就會被再次阻塞在acquireQueued方法裡)
40    如果interruptMode=REINTERRUPT的話,說明之前已經呼叫過signal方法了,也就是說該節點已經從條件佇列中剔除掉了,
41    nextWaiter指標肯定為空,所以在這種情況下是不需要執行unlinkCancelledWaiters方法的
42    而如果interruptMode=THROW_IE的話,說明之前還沒有呼叫過signal方法來從條件佇列中剔除該節點。這個時候就需要呼叫
43    unlinkCancelledWaiters方法來剔除這個節點了(在之前的transferAfterCancelledWait方法中
44    已經把該節點的狀態改為了初始狀態0),順便把所有其他不是CONDITION狀態的節點也一併剔除掉。注意:如果當前節點是條件佇列中的
45    最後一個節點的話,並不會被清理。無妨,等到下次新增節點或呼叫signal方法的時候就會被清理了
46     */
47    if (node.nextWaiter != null)
48        unlinkCancelledWaiters();
49    //根據不同模式處理中斷(正常模式不需要處理)
50    if (interruptMode != 0)
51        reportInterruptAfterWait(interruptMode);
52  }

5 addConditionWaiter方法

在條件佇列中新增一個節點的邏輯:

 1  /**
 2   * AbstractQueuedSynchronizer:
 3   */
 4  private Node addConditionWaiter() {
 5    Node t = lastWaiter;
 6    /*
 7    如果最後一個節點不是CONDITION狀態,就刪除條件佇列中所有不是CONDITION狀態的節點
 8    至於為什麼只需要判斷最後一個節點的狀態就能知道整個佇列中是否有不是CONDITION的節點,後面會說明
 9     */        
10    if (t != null && t.waitStatus != Node.CONDITION) {
11        //刪除所有不是CONDITION狀態的節點
12        unlinkCancelledWaiters();
13        t = lastWaiter;
14    }
15    //建立一個型別為CONDITION的新節點
16    Node node = new Node(Thread.currentThread(), Node.CONDITION);
17    if (t == null)
18        //t為null意味著此時條件佇列中為空,直接將頭指標指向這個新節點即可
19        firstWaiter = node;
20    else
21        //t不為null的話就說明此時條件佇列中有節點,直接在尾處加入這個新節點
22        t.nextWaiter = node;
23    //尾指標指向這個新節點,新增節點完畢
24    lastWaiter = node;
25    /*
26    注意,這裡不用像CLH佇列中的enq方法一樣,如果插入失敗就會自旋直到插入成功為止
27    因為此時還沒有釋放獨佔鎖
28     */
29    return node;
30  }
31
32  /**
33   * 第12行程式碼處:
34   * 刪除條件佇列當中所有不是CONDITION狀態的節點
35   */
36  private void unlinkCancelledWaiters() {
37    Node t = firstWaiter;
38    /*
39    在下面的每次迴圈中,trail指向的是從頭到迴圈的節點為止,最後一個是CONDITION狀態的節點
40    這樣做是因為要剔除佇列中間不是CONDITION的節點,就需要保留上一個是CONDITION節點的指標,
41    然後直接trail.nextWaiter = next就可以斷開了
42     */
43    Node trail = null;
44    while (t != null) {
45        Node next = t.nextWaiter;
46        if (t.waitStatus != Node.CONDITION) {
47            t.nextWaiter = null;
48            if (trail == null)
49                firstWaiter = next;
50            else
51                trail.nextWaiter = next;
52            if (next == null)
53                lastWaiter = trail;
54        } else
55            trail = t;
56        t = next;
57    }
58  }

6 fullyRelease方法

釋放鎖資源,包括可重入鎖的所有鎖資源:

 1  /**
 2   * AbstractQueuedSynchronizer:
 3   */
 4  final int fullyRelease(Node node) {
 5    boolean failed = true;
 6    try {
 7        int savedState = getState();
 8        /*
 9        釋放鎖資源。注意這裡是釋放所有的鎖,包括可重入鎖有多次加鎖的話,會一次性全部釋放。因為在上一行
10        程式碼savedState存的是所有的鎖資源,而這裡就是釋放這些所有的資源,這也就是方法名中“fully”的含義
11         */
12        if (release(savedState)) {
13            failed = false;
14            return savedState;
15        } else {
16            /*
17            釋放失敗就拋異常,也就是說沒有釋放乾淨,可能是在併發的情景下state被修改了的原因,
18            也可能是其他原因。注意如果在這裡丟擲異常了那麼會走第166行程式碼
19             */
20            throw new IllegalMonitorStateException();
21        }
22    } finally {
23        /*
24        如果釋放鎖失敗,就把節點置為CANCELLED狀態。比較精妙的一點是,在之前addConditionWaiter方法中的第10行程式碼處,
25        判斷條件佇列中是否有不是CONDITION的節點時,只需要判斷最後一個節點的狀態是否是CONDITION就行了
26        按常理來說,是需要遍歷整個佇列才能知道的。但是條件佇列每次新增新節點都是插在尾處,而如果釋放鎖失敗,
27        會將這個新新增的、在佇列尾巴的新節點置為CANCELLED狀態。而之前的CONDITION節點必然都是在隊頭
28        因為如果此時再有新的節點入隊的話,會首先在addConditionWaiter方法中的第12行程式碼處將所有不是CONDITION的節點都剔除了
29        也就是說無論什麼情況下,如果佇列中有不是CONDITION的節點,那它一定在隊尾,所以只需要判斷它就可以了
30         */
31        if (failed)
32            node.waitStatus = Node.CANCELLED;
33    }
34  }

7 isOnSyncQueue方法

判斷節點是否在CLH佇列中,以此來判斷喚醒時signal方法是否完成。當然,在transferAfterCancelledWait方法中也會呼叫到本方法:

 1  /**
 2   * AbstractQueuedSynchronizer:
 3   * 判斷節點是否在CLH佇列中
 4   */
 5  final boolean isOnSyncQueue(Node node) {
 6    /*
 7    如果當前節點的狀態是CONDITION或者節點沒有prev指標(prev指標只在CLH佇列中的節點有,
 8    尾插法保證prev指標一定有)的話,就返回false
 9     */
10    if (node.waitStatus == Node.CONDITION || node.prev == null)
11        return false;
12    //如果當前節點有next指標(next指標只在CLH佇列中的節點有,條件佇列中的節點是nextWaiter)的話,就返回true
13    if (node.next != null)
14        return true;
15    //如果上面無法快速判斷的話,就只能從CLH佇列中進行遍歷,一個一個地去進行判斷了
16    return findNodeFromTail(node);
17  }
18
19  /**
20   * 遍歷判斷當前節點是否在CLH佇列其中
21   */
22  private boolean findNodeFromTail(Node node) {
23    Node t = tail;
24    for (; ; ) {
25        if (t == node)
26            return true;
27        if (t == null)
28            return false;
29        t = t.prev;
30    }
31  }

8 checkInterruptWhileWaiting方法

判斷喚醒時屬於的狀態(0 / THROW_IE / REINTERRUPT):

 1  /**
 2   * AbstractQueuedSynchronizer:
 3   * 如果當前執行緒沒有被中斷過,則返回0
 4   * 如果當前執行緒被中斷時沒有被signal過,則返回THROW_IE
 5   * 如果當前執行緒被中斷時已經signal過了,則返回REINTERRUPT
 6   */
 7  private int checkInterruptWhileWaiting(Node node) {
 8    return Thread.interrupted() ?
 9            (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
10            0;
11  }
12
13  /**
14   * 本方法是用來判斷當前執行緒被中斷時有沒有發生過signal,以此來區分出THROW_IE和REINTERRUPT。判斷的依據是:
15   * 如果發生過signal,則當前節點的狀態已經不是CONDITION了,並且在CLH佇列中也能找到該節點。詳見transferForSignal方法
16   * <p>
17   * THROW_IE:表示線上程中斷髮生時還沒有呼叫過signal方法,這個時候我們將這個節點放進CLH佇列中去搶資源,
18   * 直到搶到鎖資源後,再把這個節點從CLH佇列和條件佇列中都刪除掉,最後再丟擲InterruptedException
19   * <p>
20   * REINTERRUPT:表示線上程中斷髮生時已經呼叫過signal方法了,這個時候發不發生中斷實際上已經沒有意義了,
21   * 因為該節點此時已經被放進到了CLH佇列中。而且在signal方法中已經將這個節點從條件佇列中剔除掉了
22   * 此時我們將這個節點放進CLH佇列中去搶資源,直到搶到鎖資源後(搶到資源的同時就會將這個節點從CLH佇列中刪除),
23   * 再次中斷當前執行緒即可,並不會丟擲InterruptedException
24   */
25  final boolean transferAfterCancelledWait(Node node) {
26    //判斷一下當前的節點狀態是否是CONDITION
27    if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
28        /*
29        如果CAS成功了就表示當前節點是CONDITION狀態,此時就意味著interruptMode為THROW_IE
30        然後會進行CLH佇列入隊,隨後進行搶鎖資源的操作
31         */
32        enq(node);
33        return true;
34    }
35    /*
36    如果CAS失敗了的話就意味著當前節點已經不是CONDITION狀態了,說明此時已經呼叫過signal方法了,
37    但是因為之前已經釋放鎖資源了,signal方法中的transferForSignal方法將節點狀態改為CONDITION
38    和將節點入CLH佇列的這兩個操作不是原子操作,所以可能存在併發的問題。也就是說可能會存在將節點狀態改為CONDITION後,
39    但是還沒入CLH佇列這個時間點。下面的程式碼考慮的就是這種場景。這個時候只需要不斷讓渡當前執行緒資源,
40    等待signal方法將節點新增CLH佇列完畢後即可
41     */
42    while (!isOnSyncQueue(node))
43        Thread.yield();
44    return false;
45  }

9 reportInterruptAfterWait方法

中斷喚醒最後的處理:

 1  /**
 2   * AbstractQueuedSynchronizer:
 3   */
 4  private void reportInterruptAfterWait(int interruptMode)
 5        throws InterruptedException {
 6    if (interruptMode == THROW_IE)
 7        //如果是THROW_IE最終就會丟擲InterruptedException異常
 8        throw new InterruptedException();
 9    else if (interruptMode == REINTERRUPT)
10        //如果是REINTERRUPT就僅僅是“中斷”當前執行緒而已(只是設定中斷標誌位為true)
11        selfInterrupt();
12  }

10 enqueue方法

ArrayBlockingQueue的入隊邏輯:

 1  /**
 2   * ArrayBlockingQueue:
 3   */
 4  private void enqueue(E x) {
 5    final Object[] items = this.items;
 6    //插入資料
 7    items[putIndex] = x;
 8    //putIndex記錄的是下次插入的位置。如果putIndex已經是最後一個了,重新復位為0,意味著資料可能會被覆蓋
 9    if (++putIndex == items.length)
10        putIndex = 0;
11    //當前陣列中的數量+1
12    count++;
13    /*
14    如果notEmpty條件佇列不為空的話,喚醒notEmpty條件佇列中的第一個節點去CLH佇列當中去排隊搶資源
15    如果notEmpty裡沒有節點的話,說明此時陣列沒空。signal方法將不會有任何作用,因為此時沒有阻塞住的take執行緒
16     */
17    notEmpty.signal();
18  }

11 signal方法

檢視是否需要喚醒條件佇列中的節點,需要就進行喚醒(將節點從條件佇列中轉移到CLH佇列中):

 1  /**
 2   * AbstractQueuedSynchronizer:
 3   */
 4  public final void signal() {
 5    //如果當前執行緒不是加鎖時候的執行緒,就丟擲異常
 6    if (!isHeldExclusively())
 7        throw new IllegalMonitorStateException();
 8    Node first = firstWaiter;
 9    if (first != null)
10        //如果notEmpty條件佇列中有節點的話,就通知去CLH佇列中排隊搶資源
11        doSignal(first);
12  }
13
14  private void doSignal(Node first) {
15    do {
16        if ((firstWaiter = first.nextWaiter) == null)
17            //等於null意味著迴圈到此時條件佇列已經空了,那麼把lastWaiter也置為null
18            lastWaiter = null;
19        //斷開notEmpty條件佇列中當前節點的nextWaiter指標,也就相當於剔除當前節點,等待GC
20        first.nextWaiter = null;
21    } while (!transferForSignal(first) &&
22            //如果當前節點已經不是CONDITION狀態的話(就說明當前節點已經失效了),就選擇下一個節點嘗試放進CLH佇列中
23            (first = firstWaiter) != null);
24  }
25
26  /**
27   * 將notEmpty條件佇列中的節點從條件佇列移動到CLH佇列當中
28   * 第21行程式碼處:
29   */
30  final boolean transferForSignal(Node node) {
31    /*
32    如果notEmpty條件佇列中的節點已經不是CONDITION狀態的時候,就直接返回false,
33    跳過該節點,相當於把該節點剔除出條件佇列
34     */
35    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
36        return false;
37
38    //走到這裡說明該節點的狀態已經被修改成了初始狀態0。把其加入到CLH佇列尾部,並返回前一個節點
39    Node p = enq(node);
40    int ws = p.waitStatus;
41    /*
42    再來複習一下,SIGNAL狀態表示當前節點是阻塞狀態的話,上一個節點就是SIGNAL。notEmpty條件佇列中的
43    節點此時還是處於阻塞狀態,所以此時將這個節點移動到CLH佇列後就需要將前一個節點的狀態改為SIGNAL
44    如果CAS修改失敗了的話,就將這個節點所在的執行緒喚醒去競爭鎖資源,結局肯定是沒搶到(因為鎖資源是
45    當前執行緒所持有著),所以會在acquireQueued方法中繼續被阻塞住的,而且在這其中會再次修正前一個節點
46    的SIGNAL狀態(必定是要修改成功的,如果修改不成功,就會一直在acquireQueued方法中迴圈去CAS修改)
47    當然如果前一個節點是CANCELLED狀態的話,也去喚醒這個節點。這樣acquireQueued方法中有機會去剔除掉
48    這些CANCELLED節點,相當於做了次清理工作
49    需要提一下的是,該處是喚醒被阻塞住的take執行緒(之前陣列一直是空的,現在新增了一個節點
50    後陣列就不為空了,所以需要喚醒之前被阻塞住的一個拿取執行緒。假設這個被喚醒的執行緒是執行緒2,執行喚醒動作
51    的是執行緒1)。如前面所說,執行緒2會進入到acquireQueued方法中再次被阻塞住。直到執行緒1走到put方法中的
52    最後一步unlock解鎖的時候會被再次喚醒(也不一定就是這次會被喚醒,也有可能喚醒的是其他的執行緒(假如說
53    是執行緒3)。但只要執行緒3最後執行unlock方法的時候,就會繼續去喚醒,相當於把這個喚醒的動作給傳遞下去了
54    那麼執行緒2最終就會有機會被喚醒(等到它變成CLH佇列中的第一個節點的時候))
55     */
56    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
57        LockSupport.unpark(node.thread);
58    return true;
59  }

12 take方法

ArrayBlockingQueue的take方法:

 1  /**
 2   * ArrayBlockingQueue:
 3   */
 4  public E take() throws InterruptedException {
 5    final ReentrantLock lock = this.lock;
 6    //響應中斷模式下的加鎖
 7    lock.lockInterruptibly();
 8    try {
 9        while (count == 0)
10            //如果陣列為空的話,就在notEmpty中入隊一個新節點,並阻塞當前執行緒
11            notEmpty.await();
12        //刪除陣列元素並喚醒notFull
13        return dequeue();
14    } finally {
15        //解鎖
16        lock.unlock();
17    }
18  }
19
20  /**
21   * 第13行程式碼處:
22   */
23  private E dequeue() {
24    final Object[] items = this.items;
25    //記錄舊值並最終返回出去
26    @SuppressWarnings("unchecked")
27    E x = (E) items[takeIndex];
28    //將陣列元素清空
29    items[takeIndex] = null;
30    //takeIndex記錄的是下次拿取的位置。如果takeIndex已經是最後一個了,重新復位為0
31    if (++takeIndex == items.length)
32        takeIndex = 0;
33    //當前陣列中的數量-1
34    count--;
35    //elementDequeued方法在陣列中移除資料時會被呼叫,以保證Itrs迭代器和佇列資料的一致性
36    if (itrs != null)
37        itrs.elementDequeued();
38    /*
39    如果notFull條件佇列不為空的話,喚醒notFull條件佇列中的第一個節點去CLH佇列當中去排隊搶資源
40    如果notFull裡沒有節點的話,說明此時陣列沒滿。signal方法將不會有任何作用,因為此時沒有阻塞住的put執行緒
41     */
42    notFull.signal();
43    return x;
44  }

更多內容請關注微信公眾號:奇客時間

相關文章