一、前言
有了前面分析的基礎,現在,接著分析CyclicBarrier原始碼,CyclicBarrier類在進行多執行緒程式設計時使用很多,比如,你希望建立一組任務,它們並行執行工作,然後在進行下一個步驟之前等待,直至所有的任務都完成,和join很類似,下面,開始分析原始碼。
二、CyclicBarrier資料結構
分析原始碼可以知道,CyclicBarrier底層是基於ReentrantLock和AbstractQueuedSynchronizer來實現的,所以,CyclicBarrier的資料結構也依託於AQS的資料結構,在前面對AQS的分析中已經指出了其資料結構,在這裡不再累贅。
三、CyclicBarrier原始碼分析
3.1 類的繼承關係
public class CyclicBarrier {}
說明:可以看到CyclicBarrier沒有顯示繼承哪個父類或者實現哪個父介面,根據Java語言規定,可知其父類是Object。
3.2 類的內部類
CyclicBarrier類存在一個內部類Generation,每一次使用的CycBarrier可以當成Generation的例項,其原始碼如下
private static class Generation { boolean broken = false; }
說明:Generation類有一個屬性broken,用來表示當前屏障是否被損壞。
3.3 類的屬性
public class CyclicBarrier { /** The lock for guarding barrier entry */ // 可重入鎖 private final ReentrantLock lock = new ReentrantLock(); /** Condition to wait on until tripped */ // 條件佇列 private final Condition trip = lock.newCondition(); /** The number of parties */ // 參與的執行緒數量 private final int parties; /* The command to run when tripped */ // 由最後一個進入 barrier 的執行緒執行的操作 private final Runnable barrierCommand; /** The current generation */ // 當前代 private Generation generation = new Generation(); // 正在等待進入屏障的執行緒數量 private int count; }
說明:該屬性有一個為ReentrantLock物件,有一個為Condition物件,而Condition物件又是基於AQS的,所以,歸根到底,底層還是由AQS提供支援。
3.4 類的建構函式
1. CyclicBarrier(int, Runnable)型建構函式
public CyclicBarrier(int parties, Runnable barrierAction) { // 參與的執行緒數量小於等於0,丟擲異常 if (parties <= 0) throw new IllegalArgumentException(); // 設定parties this.parties = parties; // 設定count this.count = parties; // 設定barrierCommand this.barrierCommand = barrierAction; }
說明:該建構函式可以指定關聯該CyclicBarrier的執行緒數量,並且可以指定在所有執行緒都進入屏障後的執行動作,該執行動作由最後一個進行屏障的執行緒執行。
2. CyclicBarrier(int)型建構函式
public CyclicBarrier(int parties) { // 呼叫含有兩個引數的建構函式 this(parties, null); }
說明:該建構函式僅僅執行了關聯該CyclicBarrier的執行緒數量,沒有設定執行動作。
3.5 核心函式分析
1. dowait函式
此函式為CyclicBarrier類的核心函式,CyclicBarrier類對外提供的await函式在底層都是呼叫該了doawait函式,其原始碼如下。
private int dowait(boolean timed, long nanos) throws InterruptedException, BrokenBarrierException, TimeoutException { // 儲存當前鎖 final ReentrantLock lock = this.lock; // 鎖定 lock.lock(); try { // 儲存當前代 final Generation g = generation; if (g.broken) // 屏障被破壞,丟擲異常 throw new BrokenBarrierException(); if (Thread.interrupted()) { // 執行緒被中斷 // 損壞當前屏障,並且喚醒所有的執行緒,只有擁有鎖的時候才會呼叫 breakBarrier(); // 丟擲異常 throw new InterruptedException(); } // 減少正在等待進入屏障的執行緒數量 int index = --count; if (index == 0) { // 正在等待進入屏障的執行緒數量為0,所有執行緒都已經進入 // 執行的動作標識 boolean ranAction = false; try { // 儲存執行動作 final Runnable command = barrierCommand; if (command != null) // 動作不為空 // 執行 command.run(); // 設定ranAction狀態 ranAction = true; // 進入下一代 nextGeneration(); return 0; } finally { if (!ranAction) // 沒有執行的動作 // 損壞當前屏障 breakBarrier(); } } // loop until tripped, broken, interrupted, or timed out // 無限迴圈 for (;;) { try { if (!timed) // 沒有設定等待時間 // 等待 trip.await(); else if (nanos > 0L) // 設定了等待時間,並且等待時間大於0 // 等待指定時長 nanos = trip.awaitNanos(nanos); } catch (InterruptedException ie) { if (g == generation && ! g.broken) { // 等於當前代並且屏障沒有被損壞 // 損壞當前屏障 breakBarrier(); // 丟擲異常 throw ie; } else { // 不等於當前帶後者是屏障被損壞 // We're about to finish waiting even if we had not // been interrupted, so this interrupt is deemed to // "belong" to subsequent execution. // 中斷當前執行緒 Thread.currentThread().interrupt(); } } if (g.broken) // 屏障被損壞,丟擲異常 throw new BrokenBarrierException(); if (g != generation) // 不等於當前代 // 返回索引 return index; if (timed && nanos <= 0L) { // 設定了等待時間,並且等待時間小於0 // 損壞屏障 breakBarrier(); // 丟擲異常 throw new TimeoutException(); } } } finally { // 釋放鎖 lock.unlock(); } }
說明:dowait方法的邏輯會進行一系列的判斷,大致流程如下。
2. nextGeneration函式
此函式在所有執行緒進入屏障後會被呼叫,即生成下一個版本,所有執行緒又可以重新進入到屏障中,其原始碼如下
private void nextGeneration() { // signal completion of last generation // 喚醒所有執行緒 trip.signalAll(); // set up next generation // 恢復正在等待進入屏障的執行緒數量 count = parties; // 新生一代 generation = new Generation(); }
在此函式中會呼叫AQS的signalAll方法,即喚醒所有等待執行緒。如果所有的執行緒都在等待此條件,則喚醒所有執行緒。其原始碼如下
public final void signalAll() { if (!isHeldExclusively()) // 不被當前執行緒獨佔,丟擲異常 throw new IllegalMonitorStateException(); // 儲存condition佇列頭結點 Node first = firstWaiter; if (first != null) // 頭結點不為空 // 喚醒所有等待執行緒 doSignalAll(first); }
說明:此函式判斷頭結點是否為空,即條件佇列是否為空,然後會呼叫doSignalAll函式,doSignalAll函式原始碼如下
private void doSignalAll(Node first) { // condition佇列的頭結點尾結點都設定為空 lastWaiter = firstWaiter = null; // 迴圈 do { // 獲取first結點的nextWaiter域結點 Node next = first.nextWaiter; // 設定first結點的nextWaiter域為空 first.nextWaiter = null; // 將first結點從condition佇列轉移到sync佇列 transferForSignal(first); // 重新設定first first = next; } while (first != null); }
說明:此函式會依次將條件佇列中的節點轉移到同步佇列中,會呼叫到transferForSignal函式,其原始碼如下
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; }
說明:此函式的作用就是將處於條件佇列中的節點轉移到同步佇列中,並設定結點的狀態資訊,其中會呼叫到enq函式,其原始碼如下。
private Node enq(final Node node) { for (;;) { // 無限迴圈,確保結點能夠成功入佇列 // 儲存尾結點 Node t = tail; if (t == null) { // 尾結點為空,即還沒被初始化 if (compareAndSetHead(new Node())) // 頭結點為空,並設定頭結點為新生成的結點 tail = head; // 頭結點與尾結點都指向同一個新生結點 } else { // 尾結點不為空,即已經被初始化過 // 將node結點的prev域連線到尾結點 node.prev = t; if (compareAndSetTail(t, node)) { // 比較結點t是否為尾結點,若是則將尾結點設定為node // 設定尾結點的next域為node t.next = node; return t; // 返回尾結點 } } } }
說明:此函式完成了結點插入同步佇列的過程,也很好理解。
綜合上面的分析可知,newGeneration函式的主要方法的呼叫如下,之後會通過一個例子詳細講解。
3. breakBarrier函式
此函式的作用是損壞當前屏障,會喚醒所有在屏障中的執行緒。原始碼如下
private void breakBarrier() { // 設定狀態 generation.broken = true; // 恢復正在等待進入屏障的執行緒數量 count = parties; // 喚醒所有執行緒 trip.signalAll(); }
說明:可以看到,此函式也呼叫了AQS的signalAll函式,由signal函式提供支援。
四、示例
下面通過一個例子來詳解CyclicBarrier的使用和內部工作機制,原始碼如下
package com.hust.grid.leesf.cyclicbarrier; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; /** * * @author leesf * @time 2016.4.16 */ class MyThread extends Thread { private CyclicBarrier cb; public MyThread(String name, CyclicBarrier cb) { super(name); this.cb = cb; } public void run() { System.out.println(Thread.currentThread().getName() + " going to await"); try { cb.await(); System.out.println(Thread.currentThread().getName() + " continue"); } catch (Exception e) { e.printStackTrace(); } } } public class CyclicBarrierDemo { public static void main(String[] args) throws InterruptedException, BrokenBarrierException { CyclicBarrier cb = new CyclicBarrier(3, new Thread("barrierAction") { public void run() { System.out.println(Thread.currentThread().getName() + " barrier action"); } }); MyThread t1 = new MyThread("t1", cb); MyThread t2 = new MyThread("t2", cb); t1.start(); t2.start(); System.out.println(Thread.currentThread().getName() + " going to await"); cb.await(); System.out.println(Thread.currentThread().getName() + " continue"); } }
執行結果(某一次):
t1 going to await main going to await t2 going to await t2 barrier action t2 continue t1 continue main continue
說明:根據結果可知,可能會存在如下的呼叫時序。
說明:由上圖可知,假設t1執行緒的cb.await是在main執行緒的cb.barrierAction動作是由最後一個進入屏障的執行緒執行的。根據時序圖,進一步分析出其內部工作流程。
① main(主)執行緒執行cb.await操作,主要呼叫的函式如下。
說明:由於ReentrantLock的預設採用非公平策略,所以在dowait函式中呼叫的是ReentrantLock.NonfairSync的lock函式,由於此時AQS的狀態是0,表示還沒有被任何執行緒佔用,故main執行緒可以佔用,之後在dowait中會呼叫trip.await函式,最終的結果是條件佇列中存放了一個包含main執行緒的結點,並且被禁止執行了,同時,main執行緒所擁有的資源也被釋放了,可以供其他執行緒獲取。
② t1執行緒執行cb.await操作,其中假設t1執行緒的lock.lock操作在main執行緒釋放了資源之後,則其主要呼叫的函式如下。
說明:可以看到,之後condition queue(條件佇列)裡面有兩個節點,包含t1執行緒的結點插入在佇列的尾部,並且t1執行緒也被禁止了,因為執行了park操作,此時兩個執行緒都被禁止了。
③ t2執行緒執行cb.await操作,其中假設t2執行緒的lock.lock操作在t1執行緒釋放了資源之後,則其主要呼叫的函式如下。
說明:由上圖可知,在t2執行緒執行await操作後,會直接執行command.run方法,不是重新開啟一個執行緒,而是最後進入屏障的執行緒執行。同時,會將Condition queue中的所有節點都轉移到Sync queue中,並且最後main執行緒會被unpark,可以繼續執行。main執行緒獲取cpu資源,繼續執行。
④ main執行緒獲取cpu資源,繼續執行,下圖給出了主要的方法呼叫。
說明:其中,由於main執行緒是在AQS.CO的wait中被park的,所以恢復時,會繼續在該方法中執行。執行過後,t1執行緒被unpark,它獲得cpu資源可以繼續執行。
⑤ t1執行緒獲取cpu資源,繼續執行,下圖給出了主要的方法呼叫。
說明:其中,由於t1執行緒是在AQS.CO的wait方法中被park,所以恢復時,會繼續在該方法中執行。執行過後,Sync queue中保持著一個空節點。頭結點與尾節點均指向它。
注意:線上程await過程中中斷執行緒會丟擲異常,所有進入屏障的執行緒都將被釋放。至於CyclicBarrier的其他用法,讀者可以自行查閱API,不再累贅。
五、總結
有了AQS與ReentrantLock的基礎,分析CyclicBarrier就會非常簡單,因為其底層就是由兩者支撐的,關於CycylicBarrier的原始碼就分析到此,有疑問的讀者,歡迎交流,謝謝各位園友的觀看~