Java併發學習筆記(四)-柵欄CyclicBarrier
阿新 • • 發佈:2019-01-10
閉鎖是一次性物件,一旦進入終止狀態,就不能被重置,它是用來啟動一組相關的操作,或者等待一組相關的操作結束。
柵欄跟閉鎖有點類似,它能阻塞一組執行緒直到某個時間發生,但是這裡有個很大的區別,在柵欄裡,只有這組執行緒都到達柵欄位置時,才能繼續執行
public class CyclicBarrierDemo { public static void main(String[] args) { CyclicBarrier barrier = new CyclicBarrier(5, new Runnable() { //柵欄動作,在計數器為0的時候執行 @Override public void run() { System.out.println("我們都準備好了."); } }); ExecutorService es = Executors.newCachedThreadPool(); for (int i = 0; i < 5; i++) { es.execute(new Roommate(barrier)); } } } class Roommate implements Runnable { private CyclicBarrier barrier; private static int Count = 1; private int id; public Roommate(CyclicBarrier barrier) { this.barrier = barrier; this.id = Count++; } @Override public void run() { System.out.println(id + " : 我到了"); try { //通知barrier,已經完成動作,在等待 barrier.await(); System.out.println("Id " + id + " : 點菜吧!"); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } } }
在上面的例子裡,柵欄初始計數為5,說明需要5個執行緒達到柵欄,這5個執行緒才能繼續執行。當在一個執行緒裡,呼叫barrier.await相當於告訴柵欄,已經有一個執行緒達到柵欄。當barrier.await執行5次後,柵欄開啟,所有5個執行緒都可以繼續執行。此時,還會發生兩件事,柵欄會執行柵欄動作,也就是在初始化柵欄物件時產生的Runnable類物件需要執行的run方法,還有一件事,柵欄的計數會重置為5。
綜合以上,柵欄和閉鎖有以下區別
- 柵欄可重複使用,在計數器為0的時候,會重置為原來的計數
- 柵欄有柵欄動作,在計數器為0的時候,會執行柵欄動作
那麼柵欄的實現機制跟閉鎖有什麼區別?
跟閉鎖依靠Sync不同,柵欄是依靠可重入鎖實現的。
/** The lock for guarding barrier entry */
private final ReentrantLock lock = new ReentrantLock();
/** Condition to wait on until tripped */
private final Condition trip = lock.newCondition();
ReentrantLock是併發庫提供的可重入鎖,而trip是ReentrantLock的條件,一個可重入鎖可以有多個條件。(以後補充)
建立柵欄物件的時候,傳遞計數和柵欄動作給柵欄物件。barrierCommand即柵欄動作,count儲存計數器。public CyclicBarrier(int parties, Runnable barrierAction) { if (parties <= 0) throw new IllegalArgumentException(); this.parties = parties; this.count = parties; this.barrierCommand = barrierAction; }
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen;
}
}
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;
//計數器為0
if (index == 0) { // tripped
boolean ranAction = false;
try {
//執行柵欄動作
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
//下一個週期
nextGeneration();
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
}
//計數器不為0,則迴圈直到被啟用或者打斷或者超時
for (;;) {
try {
if (!timed)
//trip條件阻塞,此時,lock鎖會釋放掉
trip.await();
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
if (g == generation && !g.broken) {
breakBarrier();
throw ie;
} else {
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方法的邏輯是,用可重入鎖鎖定後,計數器減1,並判斷此時計數器是否為0,不為0,會進入迴圈,在condition變數trip呼叫await方法時,進入阻塞狀態並釋放lock鎖。如果計數器為0,會執行柵欄動作,並呼叫nextGeneration開始下一個柵欄週期。
private void nextGeneration() {
// signal completion of last generation
trip.signalAll();
// set up next generation
count = parties;
generation = new Generation();
}
可以看到,在nextGeneration方法裡,trip呼叫了signalAll,喚醒了所有在trip.await阻塞的執行緒。