前言
在之前的介紹 CountDownLatch 的文章中,CountDown 可以實現多個執行緒協調,在所有指定執行緒完成後,主執行緒才執行任務。
但是,CountDownLatch 有個缺陷,這點 JDK 的文件中也說了:他只能使用一次。在有些場合,似乎有些浪費,需要不停的建立 CountDownLatch 例項,JDK 在 CountDownLatch 的文件中向我們介紹了 CyclicBarrier——迴圈柵欄。具體使用參見文章 併發程式設計之 執行緒協作工具類。
原始碼分析
該類結構如下:
有一個我們常用的方法 await,還有一個內部類,Generation ,僅有一個引數,有什麼作用呢?
在 CyclicBarrier 中,有一個 “代” 的概念,因為 CyclicBarrier 是可以複用的,那麼每次所有的執行緒通過了柵欄,就表示一代過去了,就像我們的新年一樣。當所有人跨過了元旦,日曆就更新了。
為什麼需要這個呢?後面我們看原始碼的時候在細說,現在說有點不太容易懂。
再看看構造方法,有 2 個構造方法:
public CyclicBarrier(int parties) {
this(parties, null);
}
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
複製程式碼
如果使用 CyclicBarrier 就知道了,CyclicBarrier 支援在所有執行緒通過柵欄的時候,執行一個執行緒的任務。
parties 屬性就是執行緒的數量,這個數量用來控制什麼時候釋放開啟柵欄,讓所有線 程通過。
好了,CyclicBarrier 的最重要的方法就是 await 方法,當執行了這樣一個方法,就像是樹立了一個柵欄,將執行緒擋住了,只有所有的執行緒都到了這個柵欄上,柵欄才會開啟。
看看這個方法的實現。
await 方法實現
程式碼加註釋如下:
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()) {
// 將損壞狀態設定為 true
// 並通知其他阻塞在此柵欄上的執行緒
breakBarrier();
throw new InterruptedException();
}
// 獲取下標
int index = --count;
// 如果是 0 ,說明到頭了
if (index == 0) { // tripped
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
// 執行柵欄任務
if (command != null)
command.run();
ranAction = true;
// 更新一代,將 count 重置,將 generation 重置.
// 喚醒之前等待的執行緒
nextGeneration();
// 結束
return 0;
} finally {
// 如果執行柵欄任務的時候失敗了,就將柵欄失效
if (!ranAction)
breakBarrier();
}
}
for (;;) {
try {
// 如果沒有時間限制,則直接等待,直到被喚醒
if (!timed)
trip.await();
// 如果有時間限制,則等待指定時間
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
// g == generation >> 當前代
// ! g.broken >>> 沒有損壞
if (g == generation && ! g.broken) {
// 讓柵欄失效
breakBarrier();
throw ie;
} else {
// 上面條件不滿足,說明這個執行緒不是這代的.
// 就不會影響當前這代柵欄執行邏輯.所以,就打個標記就好了
Thread.currentThread().interrupt();
}
}
// 當有任何一個執行緒中斷了,會呼叫 breakBarrier 方法.
// 就會喚醒其他的執行緒,其他執行緒醒來後,也要丟擲異常
if (g.broken)
throw new BrokenBarrierException();
// g != generation >>> 正常換代了
// 一切正常,返回當前執行緒所在柵欄的下標
// 如果 g == generation,說明還沒有換代,那為什麼會醒了?
// 因為一個執行緒可以使用多個柵欄,當別的柵欄喚醒了這個執行緒,就會走到這裡,所以需要判斷是否是當前代。
// 正是因為這個原因,才需要 generation 來保證正確。
if (g != generation)
return index;
// 如果有時間限制,且時間小於等於0,銷燬柵欄,並丟擲異常
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
複製程式碼
程式碼雖然長,但整體邏輯還是很簡單的。總結一下該方法吧。
-
首先,每個 CyclicBarrier 都有一個 Lock,想執行 await 方法,就必須獲得這把鎖。所以,CyclicBarrier 在併發情況下的效能是不高的。
-
一些執行緒中斷的判斷,注意,CyclicBarrier 中,只有有一個執行緒中斷了,其餘的執行緒也會丟擲中斷異常。並且,這個 CyclicBarrier 就不能再次使用了。
-
每次執行緒呼叫一次 await 方法,表示這個執行緒到了柵欄這裡了,那麼就將計數器減一。如果計數器到 0 了,表示這是這一代最後一個執行緒到達柵欄,就嘗試執行我們構造方法中輸入的任務。最後,將代更新,計數器重置,並喚醒所有之前等待在柵欄上的執行緒。
-
如果不是最後一個執行緒到達柵欄了,就使用 Condition 的 await 方法阻塞執行緒。如果等待過程中,執行緒中斷了,就丟擲異常。這裡,注意一下,如果中斷的執行緒的使用 CyclicBarrier 不是這代的,比如,在最後一次執行緒執行 signalAll 後,並且更新了這個“代”物件。在這個區間,這個執行緒被中斷了,那麼,JDK 認為任務已經完成了,就不必在乎中斷了,只需要打個標記。所以,catch 裡的 else 判斷用於極少情況下出現的判斷——任務完成,“代” 更新了,突然出現了中斷。這個時候,CyclicBarrier 是不在乎的。因為任務已經完成了。
-
當有一個執行緒中斷了,也會喚醒其他執行緒,那麼就需要判斷 broken 狀態。
-
如果這個執行緒被其他的 CyclicBarrier 喚醒了,那麼 g 肯定等於 generation,這個事件就不能 return 了,而是繼續迴圈阻塞。反之,如果是當前 CyclicBarrier 喚醒的,就返回執行緒在 CyclicBarrier 的下標。完成了一次衝過柵欄的過程。
總結
從 await 方法看,CyclicBarrier 還是比較簡單的,JDK 的思路就是:設定一個計數器,執行緒每呼叫一次計數器,就減一,並使用 Condition 阻塞執行緒。當計數器是0的時候,就喚醒所有執行緒,並嘗試執行建構函式中的任務。由於 CyclicBarrier 是可重複執行的,所以,就需要重置計數器。
CyclicBarrier 還有一個重要的點,就是 generation 的概念,由於每一個執行緒可以使用多個 CyclicBarrier,每個 CyclicBarrier 又都可以喚醒執行緒,那麼就需要用代來控制,如果代不匹配,就需要重新休眠。同時,這個代還記錄了執行緒的中斷狀態,如果任何執行緒中斷了,那麼所有的執行緒都會丟擲中斷異常,並且 CyclicBarrier 不再可用了。
總而言之,CyclicBarrier 是依靠一個計數器實現的,內部有一個 count 變數,每次呼叫都會減一。當一次完整的柵欄活動結束後,計數器重置,這樣,就可以重複利用了。
而他和 CountDownLatch 的區別在於,CountDownLatch 只能使用一次就 over 了,CyclicBarrier 能使用多次,可以說功能類似,CyclicBarrier 更強大一點。並且 CyclicBarrier 攜帶了一個在柵欄處可以執行的任務。更加靈活。
下面來一張圖,說說 CyclicBarrier 的流程。和 CountDownLatch 類似: