歡迎來到《併發王者課》,本文是該系列文章中的第21篇,鉑金中的第8篇。
在上一篇文章中,我們介紹了CountDownLatch的用法。在協調多執行緒的開始和結束時,CountDownLatch是個非常不錯的選擇。而本文即將給你介紹的CyclicBarrier則更加有趣,它在能力上和CountDownLatch既有相似之處,又有著明顯的不同,值得你一覽究竟。本文會先從場景上帶你理解問題,再去理解CyclicBarrier提供的方案。
一、CyclicBarrier初體驗
1. 峽谷森林裡的愛情
在峽谷的江湖中,不僅有生殺予奪和刀光劍影,還有著美妙的愛情故事。
峽谷戰神鎧曾經在危急關頭救了大喬,這一出英雄救美讓他們擦除了愛情的火花,有事沒事兩人就在峽谷中的各個角落幽會。其中,峽谷森林就是他們常去的地方,誰先到就等另一個,兩人都到齊後,再一起玩耍。
這裡頭,有兩個重點。一是他們要相互等待,二是都到齊後再玩耍。現在,我們試想一下,如果用程式碼來模擬這個場景的話,你打算怎麼做。有的同學可能會說,兩個人(執行緒)的等待很好處理。可是,如果是三人呢?
所以,這個場景問題可以概括為:多個執行緒相互等待,到齊後再執行特定動作。
接下來,我們就通過CyclicBarrier來模擬解決這個場景的問題,直觀感受CyclicBarrier的用法。
在下面這段程式碼中,我們定義了一個幽會地點(appointmentPlace),以及大喬和鎧這兩個主人公。在他們都達到幽會地點後,我們輸出一句包含三朵玫瑰???的話來予以確認,給他們送上祝福。
private static String appointmentPlace = "峽谷森林";
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(2, () -> print("???到達約會地點:大喬和鎧都來到了?" + appointmentPlace));
Thread 大喬 = newThread("大喬", () -> {
say("鎧,你在哪裡...");
try {
cyclicBarrier.await(); //到達幽會地點
say("鎧,你終於來了...");
} catch (Exception e) {
e.printStackTrace();
}
});
Thread 鎧 = newThread("鎧", () -> {
try {
Thread.sleep(500); //鎧打野中
say("我打個野,馬上就到!");
cyclicBarrier.await(); //到達幽會地點
say("喬,不好意思,剛打野遇上蘭陵王了,你還好嗎?!");
} catch (Exception e) {
e.printStackTrace();
}
});
大喬.start();
鎧.start();
}
輸出結果如下:
大喬:鎧,你在哪裡...
鎧:我打個野,馬上就到!
???到達約會地點:大喬和鎧都來到了?峽谷森林
鎧:喬,不好意思,剛打野遇上蘭陵王了,你還好嗎?!
大喬:鎧,你終於來了...
Process finished with exit code 0
對於程式碼的細節暫且不必深究,本文後面對CyclicBarrier的內部細節會有詳解,先感受它的基本用法。
從結果中可以看到,CyclicBarrier可以像CountDownLatch一樣,協調多執行緒的執行結束動作,在它們都結束後執行特定動作。從這點上來說,這是CyclicBarrier與CountDownLatch相似之處。然而,接下來的這個場景,所體現的則是它們一個明顯的不同之處。
2. 小河邊的幽會
在上面的場景中,鎧已經提到他在打野時遇到了蘭陵王。而在鎧與大喬的約會中,蘭陵王竟然又撞見了他們,真是冤家路窄。於是,在蘭陵王的攪局下,鎧和大喬不得不轉移陣地,他們同樣約定到新的約定地點後等待對方。(鎧一直以為蘭陵王也喜歡大喬,要和他橫刀奪愛,其實蘭陵王在乎的只是鎧打了它的野,他的心裡只有野怪,對任何女人毫無興趣)。
此時,如果繼續用程式碼模擬這一場景的話,那麼CountDownLatch就無能為力了,因為CountDownLatch的使用是一次性的,無法重複利用。而此時,你就會發現CyclicBarrier的神奇之處,它竟然可以重複利用。似乎,你可能已經大概明白它為什麼叫Cyclic的原因了。
接下來,我們再走一段程式碼,模擬大喬和鎧的第二次幽會。在程式碼中,我們仍然定義幽會地點、大喬和鎧兩個主人公。但是與此前不同的是,我們還增加了蘭陵王這個攪局者,以及中途變更了幽會地點。
private static String appointmentPlace = "峽谷森林";
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(2, () -> System.out.println("???到達約會地點:大喬和鎧都來到了?" + appointmentPlace));
Thread 大喬 = newThread("大喬", () -> {
say("鎧,你在哪裡...");
try {
cyclicBarrier.await();
say("鎧,你終於來了...");
Thread.sleep(2600); //約會中...
say("好的,你要小心!");
cyclicBarrier.await(); // 注意這裡是第二次呼叫await
Thread.sleep(100);
say("真好!");
} catch (Exception e) {
e.printStackTrace();
}
});
Thread 鎧 = newThread("鎧", () -> {
try {
Thread.sleep(500); //鎧打野中
say("我打個野,馬上就到!");
cyclicBarrier.await(); //到達幽會地點
say("喬,不好意思,剛打野遇上蘭陵王了,你還好嗎?!");
Thread.sleep(1500); //幽會中...
note("幽會中...\n");
Thread.sleep(1000); //幽會中...
say("這個該死的蘭陵王!喬,你先走,小河邊見!"); //鎧突然看到了蘭陵王
appointmentPlace = "小河邊"; // 鎧把地點改成了小河邊
Thread.sleep(1500); //和蘭陵王對決中...
note("︎\uD83D\uDDE1\uD83D\uDD2A鎧和蘭陵王決戰開始,最終鎧殺死了蘭陵王,並前往小河邊...\n");
cyclicBarrier.await(); // 殺了蘭陵王后,鎧到了小河邊 !!!注意這裡是第二次呼叫await
say("喬,我已經解決了蘭陵王,你看今晚夜色多美,我陪你看星星到天明...");
} catch (Exception ignored) {}
});
Thread 蘭陵王 = newThread("蘭陵王", () -> {
try {
Thread.sleep(2500);
note("蘭陵王出場...");
say("鎧打了我的野,不殺他誓不罷休!");
say("鎧,原來你和大喬在這裡!\uD83D\uDDE1️\uD83D\uDDE1️");
} catch (Exception ignored) {}
});
蘭陵王.start();
大喬.start();
鎧.start();
}
輸出結果如下所示。鎧峽谷森林的好事被蘭陵王攪局後,鎧怒火中燒,讓大喬先走,並約定在小河邊碰面。隨後,鎧斬殺了蘭陵王(可憐的鋼鐵直男),並前往小河邊,完成他和大喬的第二次幽會。
大喬:鎧,你在哪裡...
鎧:我打個野,馬上就到!
???到達約會地點:大喬和鎧都來到了?峽谷森林
鎧:喬,不好意思,剛打野遇上蘭陵王了,你還好嗎?!
大喬:鎧,你終於來了...
幽會中...
蘭陵王出場...
蘭陵王:鎧打了我的野,不殺他誓不罷休!
蘭陵王:鎧,原來你和大喬在這裡!?️?️
鎧:這個該死的蘭陵王!喬,你先走,小河邊見!
大喬:好的,你要小心!
︎??鎧和蘭陵王決戰開始,最終鎧殺死了蘭陵王,並前往小河邊...
???到達約會地點:大喬和鎧都來到了?小河邊
鎧:喬,我已經解決了蘭陵王,你看今晚夜色多美,我陪你看星星到天明...
大喬:真好!
Process finished with exit code 0
同樣的,你暫時不要理會程式碼的細節,但是你要注意到其中鎧和大喬對await()
的兩次呼叫。在你沒有理解它的原理之前,可能會驚訝於它的神奇,這是正常現象。
二、CyclicBarrier是如何實現的
CyclicBarrier是Java中提供的一個執行緒同步工具,與CountDownLatch相似,但又並不完全相同,最核心的區別在於CyclicBarrier是可以迴圈使用的,這一點在它的名字中也已經有所體現。
接下來,我們來分析下它具體的原始碼實現。
1. 核心資料結構
private final ReentrantLock lock = new ReentrantLock()
:進入屏障的鎖,只有一把;private final Condition trip = lock.newCondition()
:和上面的lock配套使用;private final int parties
:參與方的數量,本文上述的例子只有鎧和大喬,所以數量是2;private final Runnable barrierCommand
:在本輪結束時執行的特定程式碼。本文上述例子用到了它,可以上翻檢視;private Generation generation = new Generation()
:當前屏障的代次。比如本文上述的兩個場景中,generation是不同的,在鎧和大喬將幽會地點改成小河邊後,會生成新的generation;private int count
:正在等待的參與方數量。在每個代次中,count會從最初的參與數量(即parties)將至0,到0時本代次結束,而在新的代次或本代次被拆除(broken)時,count的值會恢復為parties的值。
2. 核心構造
public CyclicBarrier(int parties)
:指定參與方的數量;public CyclicBarrier(int parties, Runnable barrierAction)
:指定參與方的數量,並指定在本代次結束時執行的程式碼。
3. 核心方法
public int await()
:如果當前執行緒不是第一個到達屏障的話,它將會進入等待,直到其他執行緒都到達,除非發生被中斷、屏障被拆除、屏障被重設等情況;public int await(long timeout, TimeUnit unit)
:和await()類似,但是加上了時間限制;public boolean isBroken()
:當前屏障是否被拆除;public void reset()
:重設當前屏障。會先拆除屏障再設定新的屏障;public int getNumberWaiting()
:正在等待的執行緒數量。
在CyclicBarrier的各方法中,最為核心的就是dowait()
,兩個await()
的內部都是呼叫這個方法。所以,理解了dowait()
,基本上就理解了CyclicBarrier的實現關鍵。
dowait()
方法略長,稍微需要點耐心,我已經對其中部分做了註釋。當然,如果你想看原始碼的話,還是建議直接從JDK中看它的全部,這裡的原始碼只是為了輔助你理解上下文。
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; // 當執行緒呼叫await後,count減1
if (index == 0) { // tripped // 如果count為0,接下來將嘗試結束屏障,並開啟新的屏障
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
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 > 0 L)
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 <= 0 L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
對於CyclicBarrier的核心資料結構、構造和方法,都在上面,它們很重要。但是,更為重要的是,要理解CyclicBarrier的思想,也就是下面這幅值得你收藏的圖。理解了這幅圖,也就理解了CyclicBarrier.
此時,從這幅圖再回頭看第一節的兩個場景,鎧和大喬先後在峽谷森林、小河邊兩個地點幽會。那麼,如果也用一幅圖來表示的話,它應該是下面這樣:
三、CyclicBarrier與CountDownLatch有何不同
前面兩節已經提到了兩者的核心不同:
- CountDownLatch是一次性的,而CyclicBarrier則可以多次設定屏障,實現重複利用;
- CountDownLatch中的各個子執行緒不可以等待其他執行緒,只能完成自己的任務;而CyclicBarrier中的各個執行緒可以等待其他執行緒。
除此之外,它們倆還有著一些其他的不同,整體彙總後如下面的表格所示:
CyclicBarrier | CountDownLatch |
---|---|
CyclicBarrier是可重用的,其中的執行緒會等待所有的執行緒完成任務。屆時,屏障將被拆除,並可以選擇性地做一些特定的動作。 | CountDownLatch是一次性的,不同的執行緒在同一個計數器上工作,直到計數器為0. |
CyclicBarrier面向的是執行緒數 | CountDownLatch面向的是任務數 |
在使用CyclicBarrier時,你必須在構造中指定參與協作的執行緒數,這些執行緒必須呼叫await()方法 | 使用CountDownLatch是,則必須要指定任務數,至於這些任務由哪些執行緒完成無關緊要 |
CyclicBarrier可以在所有的執行緒釋放後重新使用 | CountDownLatch在計數器為0時不能再使用 |
在CyclicBarrier中,如果某個執行緒遇到了中斷、超時等問題時,則處於await的執行緒都會出現問題 | 在CountDownLatch中,如果某個執行緒出現問題,其他執行緒不受影響 |
小結
以上就是關於CyclicBarrier的全部內容。在學習CyclicBarrier時,要側重理解它所要解決的問題場景,以及它與CountDownLatch的不同,然後再去看原始碼,這也是為什麼我們沒有上來就放原始碼而是繞彎講了個故事的原因,雖然那個故事挺“狗血”。當然,如果這個狗血的故事能讓你記住這個知識點,狗血也值得了。
正文到此結束,恭喜你又上了一顆星✨
夫子的試煉
- 編寫程式碼體驗CyclicBarrier用法。
延伸閱讀與參考資料
關於作者
關注【技術八點半】,及時獲取文章更新。傳遞有品質的技術文章,記錄平凡人的成長故事,偶爾也聊聊生活和理想。早晨8:30推送作者品質原創,晚上20:30推送行業深度好文。
如果本文對你有幫助,歡迎點贊、關注、監督,我們一起從青銅到王者。