CyclicBarrier概述
CyclicBarrier可以理解為Cyclic + Barrier, 可迴圈使用 + 屏障嘛。
- 之所以是Cyclic的,是因為當所有等待執行緒執行完畢,並重置CyclicBarrier的狀態後它可以被重用。
- 之所以叫Barrier,是因為執行緒呼叫await方法後就會被阻塞,阻塞點就叫做屏障點。
可以讓一組執行緒全部到達一個屏障【同步點】,再全部衝破屏障,繼續向下執行。
案例學習
public class CycleBarrierTest2 {
private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(
2, // 計數器的初始值
new Runnable() { // 計數器值為0時需要執行的任務
@Override
public void run () {
System.out.println(Thread.currentThread() + " tripped ~");
}
}
);
public static void main (String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.submit(new Runnable() {
@SneakyThrows
@Override
public void run () {
Thread thread = Thread.currentThread();
System.out.println(thread + " step 1");
cyclicBarrier.await();
System.out.println(thread + " step 2");
cyclicBarrier.await();
System.out.println(thread + " step 3");
}
});
executorService.submit(new Runnable() {
@SneakyThrows
@Override
public void run () {
Thread thread = Thread.currentThread();
System.out.println(thread + " step 1");
cyclicBarrier.await();
System.out.println(thread + " step 2");
cyclicBarrier.await();
System.out.println(thread + " step 3");
}
});
executorService.shutdown();
}
}
測試結果如下:
Thread[pool-1-thread-2,5,main] step 1
Thread[pool-1-thread-1,5,main] step 1
Thread[pool-1-thread-1,5,main] tripped ~
Thread[pool-1-thread-1,5,main] step 2
Thread[pool-1-thread-2,5,main] step 2
Thread[pool-1-thread-2,5,main] tripped ~
Thread[pool-1-thread-2,5,main] step 3
Thread[pool-1-thread-1,5,main] step 3
- 建立了一個CyclicBarrier,指定parties為2作為初始計數值,指定Runnable任務作為所有執行緒到達屏障點時需要執行的任務。
- 建立了一個大小為2的執行緒池,向執行緒池中提交兩個任務,我們根據測試結果來說明這一過程。
- thread2執行緒率先執行await(),此時計數值減1,並不為0,因此thread2執行緒到達屏障點,陷入阻塞。
- thread1執行緒之後執行await(),此時計數值減1後為0,接著執行構造器中指定的任務,列印tripped,執行完後退出屏障點,喚醒thread2。
- 可以看到並不是和CountdownLatch一樣是一次性的,而是可重複使用的,退出屏障點後,計數值又被設定為2,之後又重複之前的步驟。
多個執行緒之間是相互等待的,加入當前計數器值為N,之後N-1個執行緒呼叫await方法都會達到屏障點而阻塞,只有當第N個執行緒呼叫await方法時,計數器值為0,第N個執行緒才會喚醒之前等待的所有執行緒,再一起向下執行。
CyclicBarrier是可複用的,所有執行緒達到屏障點之後,CyclicBarrier會被重置。
類圖結構及重要欄位
public class CyclicBarrier {
private static class Generation {
boolean broken = false;
}
/** 獨佔鎖保證同步 */
private final ReentrantLock lock = new ReentrantLock();
/** condition實現等待通知機制 */
private final Condition trip = lock.newCondition();
/** 記錄執行緒個數 */
private final int parties;
/* 達到屏障點執行的任務 */
private final Runnable barrierCommand;
/** The current generation */
private Generation generation = new Generation();
/**
* 記錄仍在等待的parties數量, 每一代count都會從初始的parties遞減至0
*/
private int count;
// 指定barrierAction, 線上程達到屏障後,優先執行barrierAction
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
// 指定parties, 希望屏障攔截的執行緒數量
public CyclicBarrier(int parties) {
this(parties, null);
}
}
- 基於ReentrantLock獨佔鎖實現同步與等待通知機制,底層基於AQS。
- int型別parties記錄執行緒個數,表示多少執行緒呼叫await方法後,所有執行緒才會衝破屏障繼續向下執行。
- int型別count初始化為parties,每當有執行緒呼叫await方法就遞減1,count為0表示所有執行緒到達屏障點。
CyclicBarrier是可複用的,因此使用兩個變數記錄執行緒個數,count變為0時,會將parties賦值給count,進行復用。
- barrierCommand是所有執行緒到達屏障點後執行的任務。
- CyclicBarrier是可複用的,Generation用於標記更新換代,generation內部的broken變數用來記錄當前屏障是否被打破。
本篇文章閱讀需要建立在一定獨佔鎖,Condition條件機制的基礎之上,這邊推薦幾篇前置文章,可以瞅一眼:
內部類Generation及相關方法
CyclicBarrier是可複用的,Generation用於標記更新換代。
// 屏障的每一次使用都會生成一個新的Generation例項: 可能是 tripped or reset
private static class Generation {
boolean broken = false;
}
void reset()
更新換代: 首先標記一下當前這代不用了, 然後換一個新的。
public void reset() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
breakBarrier(); // break掉當前的
nextGeneration(); // 開啟一個新的
} finally {
lock.unlock();
}
}
void breakBarrier()
標記一下broken為true,喚醒一下await等待執行緒,重置count。
private void breakBarrier() {
// 標記broken 為true
generation.broken = true;
// 重置count
count = parties;
// 喚醒因await等待的執行緒
trip.signalAll();
}
void nextGeneration()
喚醒一下await等待執行緒,重置count,更新為下一代。
private void nextGeneration() {
// 喚醒因await等待的執行緒
trip.signalAll();
// 重置count,意味著下一代了
count = parties;
// 下一代了
generation = new Generation();
}
int await()
當前執行緒呼叫await方法時會阻塞,除非遇到以下幾種情況:
- 所有執行緒都達到了屏障點,也就是parties個執行緒都呼叫了await()方法,使count遞減至0。
- 其他執行緒呼叫了當前執行緒的interrupt()方法,中斷當前執行緒,丟擲InterruptedException而返回。
- 與當前屏障關聯的Generation中的broken被設定為true,丟擲BrokenBarrierException而返回。
它內部呼叫了int dowait(boolean timed, long nanos)
,詳細解析往下面翻哈。
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
int await(long timeout, TimeUnit unit)
相比於普通的await()方法,該方法增加了超時的控制,你懂的。
增加了一項:如果超時了,返回false。
public int await(long timeout, TimeUnit unit)
throws InterruptedException,
BrokenBarrierException,
TimeoutException {
return dowait(true, unit.toNanos(timeout));
}
int dowait(boolean timed, long nanos)
- 第一個引數為true,說明需要超時控制。
- 第二個引數設定超時的時間。
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
// 獲取獨佔鎖
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 與當前屏障點關聯的Generation
final Generation g = generation;
// broken標誌為true,則異常
if (g.broken)
throw new BrokenBarrierException();
// 如果被打斷,則breakBarrier,並丟擲異常
if (Thread.interrupted()) {
// 打破: 1 標記broken為true 2 重置count 3 喚醒await等待的執行緒
breakBarrier();
throw new InterruptedException();
}
int index = --count;
// 說明已經到達屏障點了
if (index == 0) { // tripped
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
// 執行一下任務
if (command != null)
command.run();
ranAction = true;
// 更新: 1 喚醒await等待的執行緒 2 更新Generation
nextGeneration();
return 0;
} finally {
// 執行失敗了,可能被打斷了
if (!ranAction)
breakBarrier();
}
}
// loop until tripped, broken, interrupted, or timed out
// 死迴圈, 結束的情況有:到達屏障點, broken了, 中斷, 超時
for (;;) {
try {
// 超時控制
if (!timed)
trip.await();
else if (nanos > 0L)
// awaitNanos阻塞一段時間
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
if (g == generation && ! g.broken) {
// 標記broken為true
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();
}
}
// 正常被喚醒, 再次檢查當前這一代是否已經標記了broken
if (g.broken)
throw new BrokenBarrierException();
// 最後一個執行緒在等待執行緒醒來之前,已經通過nextGeneration將generation更新
if (g != generation)
return index;
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
以parties為N為例,我們來看看這一流程。
-
執行緒呼叫dowait方法後,首先會獲取獨佔鎖lock。如果是前N-1個執行緒,由於
index != 0
,會在條件佇列中等待trip.await() or trip.awaitNanos(nanos)
,會相應釋放鎖。 -
第N個執行緒呼叫dowait之後,此時
index == 0
,將會執行命令command.run()
,然後呼叫nextGeneration()
更新換代,同時喚醒所有條件佇列中等待的N-1個執行緒。 -
第N個執行緒釋放鎖,後續被喚醒的執行緒移入AQS佇列,陸續獲取鎖,釋放鎖。
CyclicBarrier與CountDownLatch的區別
-
CountDownLatch基於AQS,state表示計數器的值,在構造時指定。CyclicBarrier基於ReentrantLock獨佔鎖與Condition條件機制實現屏障邏輯。
-
CountDownLatch的計數器只能使用一次,而CyclicBarrier的計數器可以使用reset()方法重置,可複用效能夠處理更為複雜【分段任務有序執行】的業務場景。
-
CyclicBarrier還提供了其他有用的方法,如
getNumberWaiting
方法可以獲得CyclicBarrier阻塞的執行緒數量。isBroken()
方法用來了解阻塞的執行緒是否被中斷。
總結
-
CyclicBarrier = Cyclic + Barrier, 可重用 + 屏障,可以讓一組執行緒全部到達一個屏障【同步點】,再全部衝破屏障,繼續向下執行。
-
CyclicBarrier基於ReentrantLock獨佔鎖與Condition條件機制實現屏障邏輯。
-
CyclicBarrier需要指定parties【N】以及可選的任務,當N - 1個執行緒呼叫await的時候,會在條件佇列中阻塞,直到第N個執行緒呼叫await,執行指定的任務後,喚醒N - 1個等待的執行緒,並重置Generation,更新count。
參考閱讀
- 《Java併發程式設計之美》
- 《Java併發程式設計的藝術》