寫在開頭
面試官:同學,AQS的原理知道嗎?
我:學過一點,抽象佇列同步器,Java中很多同步工具都是基於它的...
面試官:好的,那其中CyclicBarrier學過嗎?講一講它的妙用吧
我:啊,這個,這個我平時寫程式碼沒用過...
面試官:那你回去再學學吧!
隨著Java的國內競爭環境逐漸激烈,面試時遇到很多奇葩的問題也是越來越多,以上是模擬的一個面試場景,同學們看下你們能答得上來不?😝
什麼是CyclicBarrier?
在過去的幾天裡,我們基於AQS學習了不少內容,其中基於AQS構建的同步工具類也學了Semaphore(訊號量)和CountDownLatch(倒數計時器),甚至於也手撕過同步器,今天我們繼續來學習另外一個同步類:CyclicBarrier
CyclicBarrier(迴圈屏障):讓一組執行緒到達一個屏障(也可以叫同步點)時被阻塞,直到最後一個執行緒到達屏障時,屏障才會開門,所有被屏障攔截的執行緒才會繼續幹活。
CyclicBarrier的原理
在CyclicBarrier有兩個成員變數分別為parties
,count
,前者代表每次攔截的執行緒數量,後者是初始化時保持和parties相等的計數標識,每有一個執行緒執行到同步點時,count減1,當count值變為0時說明所有執行緒都走到了同步點,這時就可以嘗試執行我們在構造方法中設計的任務啦。
【原始碼解析1】
//每次攔截的執行緒數
private final int parties;
//計數器
private int count;
//一個引數的構造
public CyclicBarrier(int parties) {
this(parties, null);
}
//多參構造,parties為攔截執行緒數,barrierAction這個 Runnable會在 CyclicBarrier 的計數器為 0 的時候執行,用來完成更復雜的任務。
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
每個執行緒透過呼叫await方法告訴CyclicBarrier已經到達屏障,然後進行阻塞等待,知道count等於0,所有執行緒都到達了屏障,因此,我們跟入await方法的原始碼中去看一下。
【原始碼解析2】
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
//await方法內部,繼續呼叫dowait方法實現功能
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();
}
// cout減1
int index = --count;
// 當 count 數量減為 0 之後說明最後一個執行緒已經到達柵欄了,也就是達到了可以執行await 方法之後的條件
if (index == 0) { // tripped
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
// 將 count 重置為 parties 屬性的初始化值
// 喚醒之前等待的執行緒
// 下一波執行開始
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) {
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) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
在dowait(boolean timed, long nanos),可以透過時間引數來設定阻塞的時間,預設為false,在這個方法內部,每次執行緒呼叫await後,都會進行--count操作,直到index為0時,會去執行command,然後喚醒執行緒繼續向下執行,CyclicBarrier 的計數器可以透過reset()方法重置,所以它能處理迴圈使用的場景。
CyclicBarrier的使用
大致的瞭解了CyclicBarrier的原理之後,我們寫個小demo測試一下它如何使用
【程式碼示例】
public class Test {
public static void main(String[] args) {
int numberOfThreads = 3; // 執行緒數量
CyclicBarrier barrier = new CyclicBarrier(numberOfThreads, () -> {
// 當所有執行緒都到達障礙點時執行的操作
System.out.println("所有執行緒都已到達屏障,進入下一階段");
});
for (int i = 0; i < numberOfThreads; i++) {
new Thread(new Task(barrier), "Thread " + (i + 1)).start();
}
}
static class Task implements Runnable {
private final CyclicBarrier barrier;
public Task(CyclicBarrier barrier) {
this.barrier = barrier;
}
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " 正在屏障處等待");
barrier.await(); // 等待所有執行緒到達障礙點
System.out.println(Thread.currentThread().getName() + " 已越過屏障.");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
輸出:
Thread 2 正在屏障處等待
Thread 1 正在屏障處等待
Thread 3 正在屏障處等待
所有執行緒都已到達屏障,進入下一階段
Thread 3 已越過屏障.
Thread 1 已越過屏障.
Thread 2 已越過屏障.
結尾彩蛋
如果本篇部落格對您有一定的幫助,大家記得留言+點贊+收藏呀。原創不易,轉載請聯絡Build哥!
如果您想與Build哥的關係更近一步,還可以關注“JavaBuild888”,在這裡除了看到《Java成長計劃》系列博文,還有提升工作效率的小筆記、讀書心得、大廠面經、人生感悟等等,歡迎您的加入!