瞭解循環屏障CyclicBarrier

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方法是可以重複使用的,每次屏障打開後可以自動關閉,以實現下次繼續阻塞的效果。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章