文章目錄
參考:Java併發32:CyclicBarrier的基本方法和應用場景實例
這邊的描述會根據下面的例子把CyclierBarrier後面的執行稱爲發車動作.
1. 作用
網上有一些文章描述的很清楚,這邊直接截圖查看:
2. 使用示例
這邊的例子只是說明使用,因爲還沒閱讀到線程池所有沒有使用線程池的方式來寫,例子有一些bug,但是說明了CyclicBarrier的使用
上一個方法描述了基本用法,下一個方法描述了reset的作用,其實她的await方法也提供了計時的功能
public static void resetPurpose() throws InterruptedException {
final CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
for (int i = 0; i < 5; i++) {
new Thread() {
@Override
public void run() {
try {
System.out.println("等待計算");
Thread.sleep(3000);
cyclicBarrier.await();
System.out.println("我計算結束了");
} catch (InterruptedException e) {
} catch (BrokenBarrierException e) {
}
}
}.start();
}
Thread.sleep(2000);
//重置不再支持
cyclicBarrier.reset();
System.out.println("不再等待");
System.out.println("我去做別的了");
}
}
3. 源碼閱讀
1. 參數說明
1.Generation 當前CyclierBarrier迭代的管理
broken參數表示是否打破屏障,不再等待
private static class Generation {
boolean broken = false;
}
2.lock 當前迭代管理的鎖對象
直接可以看出這邊使用的是非公平可重入鎖
/** The lock for guarding barrier entry */
private final ReentrantLock lock = new ReentrantLock();
3.trip 等待發車的條件
條件變量,後面我們會看到這邊怎麼使用這個條件變量去判斷
/** Condition to wait on until tripped */
private final Condition trip = lock.newCondition();
public Condition newCondition() {
return sync.newCondition();
}
final ConditionObject newCondition() {
return new ConditionObject();
}
4.barrierCommand 發車線程
Runnable接口,可以不設置,但是設置之後會等待所有線程執行之後再進行執行。
/* The command to run when tripped */
private final Runnable barrierCommand;
5.parties 需要等待的總數
等待發車前線程的總數量
/** The number of parties */
private final int parties;
6.count 剩餘等待的數量
這個參數其實是和parties相反,用來判斷是否發車的條件,進入一個等待的線程,則這個數量-1,直到當他等於0的時候,發車
/**
* Number of parties still waiting. Counts down from parties to 0
* on each generation. It is reset to parties on each new
* generation or when broken.
*/
private int count;
2. 構造方法
這邊有兩個構造方法,第一個構造方法會指明一個Runnable的接口實現,可以理解爲最終的計算線程(如果用公交車的例子就是發車動作.),當CyclicBarrier之前的線程執行完成之後,纔會執行這個總的線程
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
public CyclicBarrier(int parties) {
this(parties, null);
}
3. 等待線程集滿:await
如果不計時的話,這邊dowait方法的第一個參數傳false,表示不計時,如果需要計時則會傳入true以及時間參數.
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
這邊全局使用一個鎖,這個鎖是屬於CyclierBarrier的重入鎖,在操作線程等待的期間,需要加鎖,防止其他線程干擾到CyclierBarrier對某個線程進入等待隊列的參數設置等。
線程每進入等待一次,則count就要做-1操作,直到count ==0的時候,如果之前聲明瞭Runnable繼承類則進行運行,重置Generation,如果count不等於0,說明還沒到達等待的期望值,則進入死循環。後面我們詳細看一下每個步驟
private int dowait(boolean timed, long nanos) throws InterruptedException, BrokenBarrierException, TimeoutException {
final ReentrantLock lock = this.lock;
//進來,先獲取鎖--這個鎖是CyclicBarrier的鎖,只有一個鎖,而不是計算線程的鎖,這邊可以造成理解的誤差.
lock.lock();
try {
//generation表示當前這一次CyclicBarrier的執行,如果一旦執行結束(發車了)這個參數就會被重置.表示新的開始
final Generation g = generation;
//如果當前線程的打破屏障的標識爲true,則拋錯,結束等待,這邊的參數只會在breakBarrier中被設置爲true,表示要打開屏障,不在等待
if (g.broken) {
throw new BrokenBarrierException();
}
//如果線程被中斷則會打破屏障並拋出異常,打破屏障這個方法我們等下來看
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
//count每等待一次都會-1.直到等於0爲止
int index = --count;
// tripped
//當count等於0的時候,就是準備開車的時候了
if (index == 0) {
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
//等於0,就開始運行發車函數了
if (command != null) {
command.run();
}
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) {
//如果設置了超時,較當前時刻等待的時間數
nanos = trip.awaitNanos(nanos);
}
} catch (InterruptedException ie) {
//發生異常如果沒有 打破標識爲false,則打破屏障
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();
}
//這邊不等於表示重置成功了,因爲在上面的 nextGeneration()中會有重置的操作
if (g != generation){
return index;
}
//有計時並且時間到了,就打開屏障,拋出錯誤
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
//最終都應該解鎖
lock.unlock();
}
}
1.當未達到指定數量時等待
這邊分成計時和不計時的兩種情況,計時的話會通過lock提供的Condition對象調用awaitNanos方法,通過if (g != generation)
判斷是否進入新的迭代,進入新的迭代則正常返回,否則超時則拋出異常.trip.await();
這裏主要關注一下沒有計時的情況,會調用
for (; ; ) {
try {
//是否計時,不計時就是等待
if (!timed) {
trip.await();
} else if (nanos > 0L) {
//如果設置了超時,較當前時刻等待的時間數
nanos = trip.awaitNanos(nanos);
}
} catch (InterruptedException ie) {
//發生異常如果沒有 打破標識爲false,則打破屏障
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();
}
//這邊不等於表示重置成功了,因爲在上面的 nextGeneration()中會有重置的操作
if (g != generation){
return index;
}
//有計時並且時間到了,就打開屏障,拋出錯誤
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
1.加入條件隊列,休眠等待
我們要分析的是await這個方法,這個trip是ReentrantLock提供的ConditionObject類.
final ConditionObject newCondition() {
return new ConditionObject();
}
try {
//是否計時,不計時就是等待
if (!timed) {
trip.await();
} else if (nanos > 0L) {
//如果設置了超時,較當前時刻等待的時間數
nanos = trip.awaitNanos(nanos);
}
}
await方法會把節點加入到條件隊列並且釋放同步隊列的節點,這邊有一個步驟是park,就是把節點休眠,等待被喚醒
當節點被喚醒的時候,會判斷中斷標識和是否在同步隊列中(如果已經被signalAll添加到隊列中),則繼續往下,通過隊列爭取鎖,並通過中斷狀態判斷應該報錯還是設置中斷標識結束整個過程.
@Override
public final void await() throws InterruptedException {
//中斷狀態報錯
if (Thread.interrupted()) {
throw new InterruptedException();
}
//加入到條件等待隊列,設置waitStatus爲CONDITION
Node node = addConditionWaiter();
//這邊因爲上面的線程lock了,所以除非異常情況,否則正常是release成功,從同步隊列中解放
int savedState = fullyRelease(node);
int interruptMode = 0;
//對不在同步隊列的節點進行park(休眠)
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
//這邊還是會判斷一下中斷標識,就是中斷是在signal之前還是在sinal之後,如果沒有被中斷就是返回0,會一直等待節點被添加到同步隊列結束循環.
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) {
break;
}
}
//這邊應該應該是最後一個線程進來把count置爲0,然後進行signalAll操作(condition隊列轉同步隊列),進行鎖的獲取和線程執行
if (acquireQueued(node, savedState) && interruptMode != THROW_IE) {
interruptMode = REINTERRUPT;
}
// clean up if cancelled
//釋放被cancel的節點
if (node.nextWaiter != null) {
unlinkCancelledWaiters();
}
//判斷是否發生中斷
if (interruptMode != 0) {
reportInterruptAfterWait(interruptMode);
}
}
2.當數量達到時打開屏障
當count == 0的時候,會運行我們之前設置的Runnable接口實現類,如果不爲空的話,否則會開始新的迭代nextGeneration(),這個方法會重置一些參數,並且喚醒其他線程.
if (index == 0) {
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
//等於0,就開始運行發車函數了
if (command != null) {
command.run();
}
ranAction = true;
//這一步相當於重置,喚醒其他線程.
nextGeneration();
return 0;
} finally {
//如果這邊發生異常就會打破屏障
if (!ranAction) {
breakBarrier();
}
}
}
private void nextGeneration() {
// signal completion of last generation
//喚醒所有的線程,執行後面的操作
trip.signalAll();
// set up next generation
//重置當前迭代的參數
count = parties;
generation = new Generation();
}
1.喚醒所有線程
循環每一個條件隊列的節點(從first開始),每一個節點進行doSignalAll進行喚醒操作。
//將此條件的所有線程從等待隊列移動到所屬鎖的等待隊列。
public final void signalAll() {
//持鎖線程不等於當前線程
if (!isHeldExclusively()) {
throw new IllegalMonitorStateException();
}
//拿到第一個等待的節點
Node first = firstWaiter;
//如果第一個節點不爲空,通過第一個節點喚醒
if (first != null) {
doSignalAll(first);
}
}
doSignalAll會把條件隊列中的節點轉換到同步隊列,這也是我們上面的await退出休眠之後,去爭取隊列獲取鎖操作的來源,他加入同步隊列是在這邊進行操作的。
//移除所有節點從條件隊列到同步隊列
private void doSignalAll(Node first) {
//置空第一個最後一個節點
lastWaiter = firstWaiter = null;
//循環直到遍歷到第一個不爲null的節點
do {
Node next = first.nextWaiter;
first.nextWaiter = null;
//這個就是轉換的操作,從第一個非空節點開始
transferForSignal(first);
first = next;
} while (first != null);
}
這邊判斷沒有辦法從CONDITION通過CAS方式置爲0的原因是waitStates被改變了,改變的地方就是把這個節點cancel了,所以要把它淘汰掉。
再往後通過enq死循環把每個節點添加到尾部
如果狀態沒有被設置爲SIGNAL或者waitStates>0(節點被取消)。這個時候會喚醒該節點,一個目的是讓他執行把waitStates改變成SIGNAL,進行同步隊列的執行操作,一個也是喚醒它可能進行節點淘汰操作。
final boolean transferForSignal(Node node) {
//如果不是CONDITION狀態這個CAS操作絕對失敗的,所以被cancel的節點跳過,繼續往下一個非空節點走
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
return false;
}
//加入隊列
Node p = enq(node);
int ws = p.waitStatus;
//被取消的節點或者是無法設置SIGNAL的節點通過喚醒重新同步.
//這邊我的想法是喚醒是爲了清除cancel的節點,或者是還沒有重置爲SIGNAL的節點,有兩個想法在裏面
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) {
LockSupport.unpark(node.thread);
}
return true;
}
3.打破屏障:breakBarrier
這邊有好幾個地方會調用breakBarrier方法,就是打破屏障,包括我們後面看的reset方法。這個方法主要的目的一個是設置broke參數,一個是喚醒所有的條件隊列線程節點。
private void breakBarrier() {
generation.broken = true;
count = parties;
trip.signalAll();
}
4.圖解
看圖應該能更好的理解上面的過程
4. reset
reset可以重置強行釋放屏障,這邊的兩個函數我們上面也講了就不多贅述了。
public void reset() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
breakBarrier(); // break the current generation
nextGeneration(); // start a new generation
} finally {
lock.unlock();
}
}