再談AbstractQueuedSynchronizer2:共享模式與基於Condition的等待/通知機制實現

五月的倉頡發表於2017-07-02

共享模式acquire實現流程

上文我們講解了AbstractQueuedSynchronizer獨佔模式的acquire實現流程,本文趁熱打鐵繼續看一下AbstractQueuedSynchronizer共享模式acquire的實現流程。連續兩篇文章的學習,也可以對比獨佔模式acquire和共享模式acquire的區別,加深對於AbstractQueuedSynchronizer的理解。

先看一下共享模式acquire的實現,方法為acquireShared和acquireSharedInterruptibly,兩者差別不大,區別就在於後者有中斷處理,以acquireShared為例:

 1 public final void acquireShared(int arg) {
 2     if (tryAcquireShared(arg) < 0)
 3         doAcquireShared(arg);
 4 }

這裡就能看出第一個差別來了:獨佔模式acquire的時候子類重寫的方法tryAcquire返回的是boolean,即是否tryAcquire成功;共享模式acquire的時候,返回的是一個int型變數,判斷是否<0。doAcquireShared方法的實現為:

 1 private void doAcquireShared(int arg) {
 2     final Node node = addWaiter(Node.SHARED);
 3     boolean failed = true;
 4     try {
 5         boolean interrupted = false;
 6         for (;;) {
 7             final Node p = node.predecessor();
 8             if (p == head) {
 9                 int r = tryAcquireShared(arg);
10                 if (r >= 0) {
11                     setHeadAndPropagate(node, r);
12                     p.next = null; // help GC
13                     if (interrupted)
14                         selfInterrupt();
15                     failed = false;
16                     return;
17                 }
18             }
19             if (shouldParkAfterFailedAcquire(p, node) &&
20                 parkAndCheckInterrupt())
21                 interrupted = true;
22         }
23     } finally {
24         if (failed)
25             cancelAcquire(node);
26     }
27 }

我們來分析一下這段程式碼做了什麼:

  1. addWaiter,把所有tryAcquireShared<0的執行緒例項化出一個Node,構建為一個FIFO佇列,這和獨佔鎖是一樣的
  2. 拿當前節點的前驅節點,只有前驅節點是head的節點才能tryAcquireShared,這和獨佔鎖也是一樣的
  3. 前驅節點不是head的,執行"shouldParkAfterFailedAcquire() && parkAndCheckInterrupt()",for(;;)迴圈,"shouldParkAfterFailedAcquire()"方法執行2次,當前執行緒阻塞,這和獨佔鎖也是一樣的

確實,共享模式下的acquire和獨佔模式下的acquire大部分邏輯差不多,最大的差別在於tryAcquireShared成功之後,獨佔模式的acquire是直接將當前節點設定為head節點即可,共享模式會執行setHeadAndPropagate方法,顧名思義,即在設定head之後多執行了一步propagate操作。setHeadAndPropagate方法原始碼為:

 1 private void setHeadAndPropagate(Node node, int propagate) {
 2     Node h = head; // Record old head for check below
 3     setHead(node);
 4     /*
 5      * Try to signal next queued node if:
 6      *   Propagation was indicated by caller,
 7      *     or was recorded (as h.waitStatus) by a previous operation
 8      *     (note: this uses sign-check of waitStatus because
 9      *      PROPAGATE status may transition to SIGNAL.)
10      * and
11      *   The next node is waiting in shared mode,
12      *     or we don't know, because it appears null
13      *
14      * The conservatism in both of these checks may cause
15      * unnecessary wake-ups, but only when there are multiple
16      * racing acquires/releases, so most need signals now or soon
17      * anyway.
18      */
19     if (propagate > 0 || h == null || h.waitStatus < 0) {
20         Node s = node.next;
21         if (s == null || s.isShared())
22             doReleaseShared();
23     }
24 }

第3行的程式碼設定重設head,第2行的程式碼由於第3行的程式碼要重設head,因此先定義一個Node型變數h獲得原head的地址,這兩行程式碼很簡單。

第19行~第23行的程式碼是獨佔鎖和共享鎖最不一樣的一個地方,我們再看獨佔鎖acquireQueued的程式碼:

 1 final boolean acquireQueued(final Node node, int arg) {
 2     boolean failed = true;
 3     try {
 4         boolean interrupted = false;
 5         for (;;) {
 6             final Node p = node.predecessor();
 7             if (p == head && tryAcquire(arg)) {
 8                 setHead(node);
 9                 p.next = null; // help GC
10                 failed = false;
11                 return interrupted;
12             }
13             if (shouldParkAfterFailedAcquire(p, node) &&
14                 parkAndCheckInterrupt())
15                 interrupted = true;
16         }
17     } finally {
18         if (failed)
19             cancelAcquire(node);
20     }
21 }

這意味著獨佔鎖某個節點被喚醒之後,它只需要將這個節點設定成head就完事了,而共享鎖不一樣,某個節點被設定為head之後,如果它的後繼節點是SHARED狀態的,那麼將繼續通過doReleaseShared方法嘗試往後喚醒節點,實現了共享狀態的向後傳播

 

共享模式release實現流程

上面講了共享模式下acquire是如何實現的,下面再看一下release的實現流程,方法為releaseShared:

1 public final boolean releaseShared(int arg) {
2     if (tryReleaseShared(arg)) {
3         doReleaseShared();
4         return true;
5     }
6     return false;
7 }

tryReleaseShared方法是子類實現的,如果tryReleaseShared成功,那麼執行doReleaseShared()方法:

 1 private void doReleaseShared() {
 2     /*
 3      * Ensure that a release propagates, even if there are other
 4      * in-progress acquires/releases.  This proceeds in the usual
 5      * way of trying to unparkSuccessor of head if it needs
 6      * signal. But if it does not, status is set to PROPAGATE to
 7      * ensure that upon release, propagation continues.
 8      * Additionally, we must loop in case a new node is added
 9      * while we are doing this. Also, unlike other uses of
10      * unparkSuccessor, we need to know if CAS to reset status
11      * fails, if so rechecking.
12      */
13     for (;;) {
14         Node h = head;
15         if (h != null && h != tail) {
16             int ws = h.waitStatus;
17             if (ws == Node.SIGNAL) {
18                 if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
19                     continue;            // loop to recheck cases
20                 unparkSuccessor(h);
21             }
22             else if (ws == 0 &&
23                      !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
24                 continue;                // loop on failed CAS
25         }
26         if (h == head)                   // loop if head changed
27             break;
28     }
29 }

主要是兩層邏輯:

  1. 頭結點本身的waitStatus是SIGNAL且能通過CAS演算法將頭結點的waitStatus從SIGNAL設定為0,喚醒頭結點的後繼節點
  2. 頭結點本身的waitStatus是0的話,嘗試將其設定為PROPAGATE狀態的,意味著共享狀態可以向後傳播

 

Condition的await()方法實現原理----構建等待佇列

我們知道,Condition是用於實現通知/等待機制的,和Object的wait()/notify()一樣,由於本文之前描述AbstractQueuedSynchronizer的共享模式的篇幅不是很長,加之Condition也是AbstractQueuedSynchronizer的一部分,因此將Condition也放在這裡寫了。

Condition分為await()和signal()兩部分,前者用於等待、後者用於喚醒,首先看一下await()是如何實現的。Condition本身是一個介面,其在AbstractQueuedSynchronizer中的實現為ConditionObject:

1 public class ConditionObject implements Condition, java.io.Serializable {
2         private static final long serialVersionUID = 1173984872572414699L;
3         /** First node of condition queue. */
4         private transient Node firstWaiter;
5         /** Last node of condition queue. */
6         private transient Node lastWaiter;
7         
8         ...
9 }

這裡貼了一些欄位定義,後面都是方法就不貼了,會對重點方法進行分析的。從欄位定義我們可以看到,ConditionObject全域性性地記錄了第一個等待的節點與最後一個等待的節點

像ReentrantLock每次要使用ConditionObject,直接new一個ConditionObject出來即可。我們關注一下await()方法的實現:

 1 public final void await() throws InterruptedException {
 2     if (Thread.interrupted())
 3         throw new InterruptedException();
 4     Node node = addConditionWaiter();
 5     int savedState = fullyRelease(node);
 6     int interruptMode = 0;
 7     while (!isOnSyncQueue(node)) {
 8         LockSupport.park(this);
 9         if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
10             break;
11     }
12     if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
13         interruptMode = REINTERRUPT;
14     if (node.nextWaiter != null) // clean up if cancelled
15         unlinkCancelledWaiters();
16     if (interruptMode != 0)
17         reportInterruptAfterWait(interruptMode);
18 }

第2行~第3行的程式碼用於處理中斷,第4行程式碼比較關鍵,新增Condition的等待者,看一下實現:

 1 private Node addConditionWaiter() {
 2     Node t = lastWaiter;
 3     // If lastWaiter is cancelled, clean out.
 4     if (t != null && t.waitStatus != Node.CONDITION) {
 5         unlinkCancelledWaiters();
 6         t = lastWaiter;
 7     }
 8     Node node = new Node(Thread.currentThread(), Node.CONDITION);
 9     if (t == null)
10         firstWaiter = node;
11     else
12         t.nextWaiter = node;
13     lastWaiter = node;
14     return node;
15 }

首先拿到佇列(注意資料結構,Condition構建出來的也是一個佇列)中最後一個等待者,緊接著第4行的的判斷,判斷最後一個等待者的waitStatus不是CONDITION的話,執行第5行的程式碼,解綁取消的等待者,因為通過第8行的程式碼,我們看到,new出來的Node的狀態都是CONDITION的

那麼unlinkCancelledWaiters做了什麼?裡面的流程就不看了,就是一些指標遍歷並判斷狀態的操作,總結一下就是:從頭到尾遍歷每一個Node,遇到Node的waitStatus不是CONDITION的就從佇列中踢掉,該節點的前後節點相連。

接著第8行的程式碼前面說過了,new出來了一個Node,儲存了當前執行緒,waitStatus是CONDITION,接著第9行~第13行的操作很好理解:

  1. 如果lastWaiter是null,說明FIFO佇列中沒有任何Node,firstWaiter=Node
  2. 如果lastWaiter不是null,說明FIFO佇列中有Node,原lastWaiter的next指向Node
  3. 無論如何,新加入的Node程式設計lastWaiter,即新加入的Node一定是在最後面

用一張圖表示一下構建的資料結構就是:

對比學習,我們總結一下Condition構建出來的佇列和AbstractQueuedSynchronizer構建出來的佇列的差別,主要體現在2點上:

  1. AbstractQueuedSynchronizer構建出來的佇列,頭節點是一個沒有Thread的空節點,其標識作用,而Condition構建出來的佇列,頭節點就是真正等待的節點
  2. AbstractQueuedSynchronizer構建出來的佇列,節點之間有next與pred相互標識該節點的前一個節點與後一個節點的地址,而Condition構建出來的佇列,只使用了nextWaiter標識下一個等待節點的地址

整個過程中,我們看到沒有使用任何CAS操作,firstWaiter和lastWaiter也沒有用volatile修飾,其實原因很簡單:要await()必然要先lock(),既然lock()了就表示沒有競爭,沒有競爭自然也沒必要使用volatile+CAS的機制去保證什麼

 

Condition的await()方法實現原理----執行緒等待

前面我們看了Condition構建等待佇列的過程,接下來我們看一下等待的過程,await()方法的程式碼比較短,再貼一下:

 1 public final void await() throws InterruptedException {
 2     if (Thread.interrupted())
 3         throw new InterruptedException();
 4     Node node = addConditionWaiter();
 5     int savedState = fullyRelease(node);
 6     int interruptMode = 0;
 7     while (!isOnSyncQueue(node)) {
 8         LockSupport.park(this);
 9         if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
10             break;
11     }
12     if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
13         interruptMode = REINTERRUPT;
14     if (node.nextWaiter != null) // clean up if cancelled
15         unlinkCancelledWaiters();
16     if (interruptMode != 0)
17         reportInterruptAfterWait(interruptMode);
18 }

構建完畢佇列之後,執行第5行的fullyRelease方法,顧名思義:fullyRelease方法的作用是完全釋放Node的狀態。方法實現為:

 1 final int fullyRelease(Node node) {
 2     boolean failed = true;
 3     try {
 4         int savedState = getState();
 5         if (release(savedState)) {
 6             failed = false;
 7             return savedState;
 8         } else {
 9             throw new IllegalMonitorStateException();
10         }
11     } finally {
12         if (failed)
13             node.waitStatus = Node.CANCELLED;
14     }
15 }

這裡第4行獲取state,第5行release的時候將整個state傳過去,理由是某執行緒可能多次呼叫了lock()方法,比如呼叫了10次lock,那麼此執行緒就將state加到了10,所以這裡要將10傳過去,將狀態全部釋放,這樣後面的執行緒才能重新從state=0開始競爭鎖,這也是方法被命名為fullyRelease的原因,因為要完全釋放鎖,釋放鎖之後,如果有競爭鎖的執行緒,那麼就喚醒第一個,這都是release方法的邏輯了,前面的文章詳細講解過。

接著看await()方法的第7行判斷"while(!isOnSyncQueue(node))":

 1 final boolean isOnSyncQueue(Node node) {
 2     if (node.waitStatus == Node.CONDITION || node.prev == null)
 3         return false;
 4     if (node.next != null) // If has successor, it must be on queue
 5         return true;
 6     /*
 7      * node.prev can be non-null, but not yet on queue because
 8      * the CAS to place it on queue can fail. So we have to
 9      * traverse from tail to make sure it actually made it.  It
10      * will always be near the tail in calls to this method, and
11      * unless the CAS failed (which is unlikely), it will be
12      * there, so we hardly ever traverse much.
13      */
14     return findNodeFromTail(node);
15 }

注意這裡的判斷是Node是否在AbstractQueuedSynchronizer構建的佇列中而不是Node是否在Condition構建的佇列中,如果Node不在AbstractQueuedSynchronizer構建的佇列中,那麼呼叫LockSupport的park方法阻塞。

至此呼叫await()方法的執行緒構建Condition等待佇列--釋放鎖--等待的過程已經全部分析完畢。

 

Condition的signal()實現原理

上面的程式碼分析了構建Condition等待佇列--釋放鎖--等待的過程,接著看一下signal()方法通知是如何實現的:

1 public final void signal() {
2     if (!isHeldExclusively())
3         throw new IllegalMonitorStateException();
4     Node first = firstWaiter;
5     if (first != null)
6         doSignal(first);
7 }

首先從第2行的程式碼我們看到,要能signal(),當前執行緒必須持有獨佔鎖,否則丟擲異常IllegalMonitorStateException。

那麼真正操作的時候,獲取第一個waiter,如果有waiter,呼叫doSignal方法:

1 private void doSignal(Node first) {
2     do {
3         if ( (firstWaiter = first.nextWaiter) == null)
4             lastWaiter = null;
5         first.nextWaiter = null;
6     } while (!transferForSignal(first) &&
7              (first = firstWaiter) != null);
8 }

第3行~第5行的程式碼很好理解:

  1. 重新設定firstWaiter,指向第一個waiter的nextWaiter
  2. 如果第一個waiter的nextWaiter為null,說明當前佇列中只有一個waiter,lastWaiter置空
  3. 因為firstWaiter是要被signal的,因此它沒什麼用了,nextWaiter置空

接著執行第6行和第7行的程式碼,這裡重點就是第6行的transferForSignal方法:

 1 final boolean transferForSignal(Node node) {
 2     /*
 3      * If cannot change waitStatus, the node has been cancelled.
 4      */
 5     if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
 6         return false;
 7 
 8     /*
 9      * Splice onto queue and try to set waitStatus of predecessor to
10      * indicate that thread is (probably) waiting. If cancelled or
11      * attempt to set waitStatus fails, wake up to resync (in which
12      * case the waitStatus can be transiently and harmlessly wrong).
13      */
14     Node p = enq(node);
15     int ws = p.waitStatus;
16     if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
17         LockSupport.unpark(node.thread);
18     return true;
19 }

方法本意是將一個節點從Condition佇列轉換為AbstractQueuedSynchronizer佇列,總結一下方法的實現:

  1. 嘗試將Node的waitStatus從CONDITION置為0,這一步失敗直接返回false
  2. 當前節點進入呼叫enq方法進入AbstractQueuedSynchronizer佇列
  3. 當前節點通過CAS機制將waitStatus置為SIGNAL

最後上面的步驟全部成功,返回true,返回true喚醒等待節點成功。從喚醒的程式碼我們可以得出一個重要結論:某個await()的節點被喚醒之後並不意味著它後面的程式碼會立即執行,它會被加入到AbstractQueuedSynchronizer佇列的尾部,只有前面等待的節點獲取鎖全部完畢才能輪到它

程式碼分析到這裡,我想類似的signalAll方法也沒有必要再分析了,顯然signalAll方法的作用就是將所有Condition佇列中等待的節點逐一佇列中從移除,由CONDITION狀態變為SIGNAL狀態並加入AbstractQueuedSynchronizer佇列的尾部。

 

程式碼示例

可能大家看了我分析半天程式碼會有點迷糊,這裡最後我貼一段我用於驗證上面Condition結論的示例程式碼,首先建立一個Thread,我將之命名為ConditionThread:

 1 /**
 2  * @author 五月的倉頡http://www.cnblogs.com/xrq730/p/7067904.html
 3  */
 4 public class ConditionThread implements Runnable {
 5 
 6     private Lock lock;
 7     
 8     private Condition condition;
 9     
10     public ConditionThread(Lock lock, Condition condition) {
11         this.lock = lock;
12         this.condition = condition;
13     }
14     
15     @Override
16     public void run() {
17         
18         if ("執行緒0".equals(JdkUtil.getThreadName())) {
19             thread0Process();
20         } else if ("執行緒1".equals(JdkUtil.getThreadName())) {
21             thread1Process();
22         } else if ("執行緒2".equals(JdkUtil.getThreadName())) {
23             thread2Process();
24         }
25         
26     }
27     
28     private void thread0Process() {
29         try {
30             lock.lock();
31             System.out.println("執行緒0休息5秒");
32             JdkUtil.sleep(5000);
33             condition.signal();
34             System.out.println("執行緒0喚醒等待執行緒");
35         } finally {
36             lock.unlock();
37         }
38     }
39     
40     private void thread1Process() {
41         try {
42             lock.lock();
43             System.out.println("執行緒1阻塞");
44             condition.await();
45             System.out.println("執行緒1被喚醒");
46         } catch (InterruptedException e) {
47             
48         } finally {
49             lock.unlock();
50         }
51     }
52     
53     private void thread2Process() {
54         try {
55             System.out.println("執行緒2想要獲取鎖");
56             lock.lock();
57             System.out.println("執行緒2獲取鎖成功");
58         } finally {
59             lock.unlock();
60         }
61     }
62     
63 }

這個類裡面的方法就不解釋了,反正就三個方法片段,根據執行緒名判斷,每個線層執行的是其中的一個程式碼片段。寫一段測試程式碼:

 1 /**
 2  * @author 五月的倉頡http://www.cnblogs.com/xrq730/p/7067904.html
 3  */
 4 @Test
 5 public void testCondition() throws Exception {
 6     Lock lock = new ReentrantLock();
 7     Condition condition = lock.newCondition();
 8         
 9     // 執行緒0的作用是signal
10     Runnable runnable0 = new ConditionThread(lock, condition);
11     Thread thread0 = new Thread(runnable0);
12     thread0.setName("執行緒0");
13     // 執行緒1的作用是await
14     Runnable runnable1 = new ConditionThread(lock, condition);
15     Thread thread1 = new Thread(runnable1);
16     thread1.setName("執行緒1");
17     // 執行緒2的作用是lock
18     Runnable runnable2 = new ConditionThread(lock, condition);
19     Thread thread2 = new Thread(runnable2);
20     thread2.setName("執行緒2");
21         
22     thread1.start();
23     Thread.sleep(1000);
24     thread0.start();
25     Thread.sleep(1000);
26     thread2.start();
27         
28     thread1.join();
29 }

測試程式碼的意思是:

  1. 執行緒1先啟動,獲取鎖,呼叫await()方法等待
  2. 執行緒0後啟動,獲取鎖,休眠5秒準備signal()
  3. 執行緒2最後啟動,獲取鎖,由於執行緒0未使用完畢鎖,因此執行緒2排隊,可以此時由於執行緒0還未signal(),因此執行緒1線上程0執行signal()後,在AbstractQueuedSynchronizer佇列中的順序是線上程2後面的

程式碼執行結果為:

 1 執行緒1阻塞
 2 執行緒0休息5秒
 3 執行緒2想要獲取鎖
 4 執行緒0喚醒等待執行緒
 5 執行緒2獲取鎖成功
 6 執行緒1被喚醒

符合我們的結論:signal()並不意味著被喚醒的執行緒立即執行。由於執行緒2先於執行緒0排隊,因此看到第5行列印的內容,執行緒2先獲取鎖。

相關文章