CyclicBarrier是常用的同步輔助工具類,它的作用是讓一組線程達到屏障點時被阻塞,直到指定數量的線程都達到屏障點,才取消阻塞,此時被阻塞的線程才能繼續執行。循環屏障可以重複使用。
SyclicBarrier構造方法有兩個CyclicBarrier(int parties)和CyclicBarrier(int parties, Runnable barrierAction)。第一個參數parties指定可以打開屏障的線程數量,第二個參數barrierAction指定一旦打開屏障就會執行(非立即)的操作。
只有調用await()方法的線程纔會達到屏障點進行等待,當等待的線程數量達到循環屏障設置的值parties,屏障纔會打開,並執行打開屏障的操作。而getNumberWaiting()方法返回當前阻塞的線程數量。
如下模擬一個循環屏障反覆使用的實例:5個學生相約一起趕校車,一起上學,一起點名,一起學習。
public class TestCyclicBarrier {
public static void main(String[] args) throws InterruptedException {
System.out.println("太陽公公出門啦");
CyclicBarrier cyclicBarrier = new CyclicBarrier(5, new BarrierPoint(0));
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new Student(i + "號學生", cyclicBarrier));
thread.start();
}
}
static class BarrierPoint implements Runnable {
private int id;
public BarrierPoint(int id) {
this.id = id;
}
@Override
public void run() {
if (id == 0) {
System.out.println("發車啦");
id = 1;
return;
}
if (id == 1) {
System.out.println("開始點名");
id = 2;
return;
}
if (id == 2) {
System.out.println("準備上課");
return;
}
}
}
static class Student implements Runnable {
private String name;
private CyclicBarrier cyclicBarrier;
public Student(String name, CyclicBarrier cyclicBarrier) {
this.name = name;
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
try {
Thread.sleep((long) (Math.random() * 2000 + 1000));
System.out.println(name + "開始上車");
cyclicBarrier.await();
Thread.sleep((long) (Math.random() * 2000 + 1000));
System.out.println(name + "進入教室");
cyclicBarrier.await();
Thread.sleep((long) (Math.random() * 2000 + 1000));
System.out.println(name + "翻開課本");
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
}
控制檯輸出的執行結果如下:
太陽公公出門啦
1號學生開始上車
3號學生開始上車
0號學生開始上車
4號學生開始上車
2號學生開始上車
發車啦
0號學生進入教室
1號學生進入教室
4號學生進入教室
2號學生進入教室
3號學生進入教室
開始點名
1號學生翻開課本
4號學生翻開課本
0號學生翻開課本
3號學生翻開課本
2號學生翻開課本
準備上課
在這個案例中,由於await()方法調用了3次,所以5條線程都被阻塞了3次。第一次發生阻塞是由於校車需要等待這5個學生都上車了纔可以發車,第二次阻塞是由於5個學生都需要到達教室才能開始點名,第三次阻塞是由於5個學生都準備好課本老師纔開始講課。
本例中通過BarrierPoint來維護每次打開屏障需要執行的操作,由於每次打開屏障所執行的操作都可能不一樣,所以通過id來標識具體的操作內容。
如果調用await()方法的線程數量大於循環屏障設置的值,被阻塞的線程將永遠阻塞。且屏障打開後的操作線程不是立即執行的。
以下是一個模擬永遠阻塞的實例:
public class TestCyclicBarrier2 {
public static void main(String[] args) {
CyclicBarrier cb = new CyclicBarrier(3, new Runnable() {
@Override
public void run() {
System.out.println("屏障打開");
}
});
for (int i = 0; i < 10; i++) {
Thread worker = new Thread(new Worker(cb));
worker.start();
}
}
static class Worker implements Runnable {
private CyclicBarrier cb;
public Worker(CyclicBarrier cb) {
super();
this.cb = cb;
}
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + "到達屏障");
cb.await();
Thread.sleep(2000);
System.out.println("屏障等待的線程數量:" + cb.getNumberWaiting());
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}
}
控制檯輸入如下:
Thread-0到達屏障
Thread-1到達屏障
Thread-3到達屏障
Thread-2到達屏障
Thread-4到達屏障
屏障打開
Thread-5到達屏障
屏障打開
Thread-6到達屏障
Thread-9到達屏障
Thread-7到達屏障
屏障打開
Thread-8到達屏障
屏障等待的線程數量:1
屏障等待的線程數量:1
屏障等待的線程數量:1
屏障等待的線程數量:1
屏障等待的線程數量:1
屏障等待的線程數量:1
屏障等待的線程數量:1
屏障等待的線程數量:1
屏障等待的線程數量:1
可以看出控制檯輸出這些內容後沒有停止而是一直運行,因爲有一個線程阻塞了,那就是Thread-8。我們創建了10條線程,但是循環屏障只能同時讓3條線程互相等待,所以有9條線程執行完畢後,必然有剩餘1條線程到達屏障時被阻塞,此處Thread-8就是那條很不幸被永久阻塞的線程。
另外從輸出內容看,輸出【屏障打開】的語句間歇性每隔3個線程就執行,這是爲什麼呢?
之前有提到過 屏障打開後的操作線程不是立即執行的。本例中 【Thread-0到達屏障】,【Thread-1到達屏障】,【Thread-3到達屏障】後其實循環屏障已經打開了,但是【Thread-2到達屏障】和【Thread-4到達屏障】的線程搶先執行,所以【屏障打開】這個執行線程慢了一拍就導致看到圖中的輸出效果。同理可以分析後面輸出錯亂的原因。但無論如何Thread-8是被永久阻塞的線程,這個結論肯定是沒錯的,因爲先到達屏障的線程先釋放。
另外 CountDownLatch和CyclicBarrier 是比較相似的,但有明顯的不同點:
CountDownLatch必須通過countDown和await方法協同工作控制阻塞時機,且CountDownLatch是一次性的,不能被複位。
CyclicBarrier只需調用await方法就能控制阻塞時機,且CyclicBarrier的await方法是可以重複使用的,每次屏障打開後可以自動關閉,以實現下次繼續阻塞的效果。