CyclicBarrier的剋星—BrokenBarrierException

上篇 CyclicBarrier多任務協同的利器 我們藉助部門TB的例子,一步步分析了 CyclicBarrier 多線程協調的功能。
CyclicBarrier 功能強大的同時,意味着提供了更多的API,並且在使用過程中,可能有一些注意點。

今天就來聊聊 BrokenBarrierException,從名字就能看出,是“屏障被破壞異常”,屏障被破壞時,CyclicBarrier 的期望功能就不能完成,甚至導致程序異常;
BrokenBarrierException 可謂是 CyclicBarrier 的剋星。

上篇的例子,我們僅僅使用了 CyclicBarrier 最基本的API

public CyclicBarrier(int parties);
CyclicBarrier(int parties, Runnable barrierAction);
public int await();

實際還有:

int getParties():獲取CyclicBarrier打開屏障的線程數量,也成爲方數
int getNumberWaiting():獲取正在CyclicBarrier上等待的線程數量
int await(timeout,TimeUnit):帶限時的阻塞等待
boolean isBroken():獲取是否破損標誌位broken的值
void reset():使得CyclicBarrier迴歸初始狀態

我們重點介紹一下,能夠導致 BrokenBarrierException 的操作,然後給出詳細示例:

首先是 await() 和 await(timeout,TimeUnit)帶時限的阻塞等待

    /**
     * Waits until all {@linkplain #getParties parties} have invoked
     * {@code await} on this barrier.
     *
     * <p>If the current thread is not the last to arrive then it is
     * disabled for thread scheduling purposes and lies dormant until
     * one of the following things happens:
     * <ul>
     * <li>The last thread arrives; or
     * <li>Some other thread {@linkplain Thread#interrupt interrupts}
     * the current thread; or
     * <li>Some other thread {@linkplain Thread#interrupt interrupts}
     * one of the other waiting threads; or
     * <li>Some other thread times out while waiting for barrier; or
     * <li>Some other thread invokes {@link #reset} on this barrier.
     * </ul>
     *
     * <p>If the current thread:
     * <ul>
     * <li>has its interrupted status set on entry to this method; or
     * <li>is {@linkplain Thread#interrupt interrupted} while waiting
     * </ul>
     * then {@link InterruptedException} is thrown and the current thread's
     * interrupted status is cleared.
     *
     * <p>If the barrier is {@link #reset} while any thread is waiting,
     * or if the barrier {@linkplain #isBroken is broken} when
     * {@code await} is invoked, or while any thread is waiting, then
     * {@link BrokenBarrierException} is thrown.
     *
     * <p>If any thread is {@linkplain Thread#interrupt interrupted} while waiting,
     * then all other waiting threads will throw
     * {@link BrokenBarrierException} and the barrier is placed in the broken
     * state.
     *
     * <p>If the current thread is the last thread to arrive, and a
     * non-null barrier action was supplied in the constructor, then the
     * current thread runs the action before allowing the other threads to
     * continue.
     * If an exception occurs during the barrier action then that exception
     * will be propagated in the current thread and the barrier is placed in
     * the broken state.
     *
     * @return the arrival index of the current thread, where index
     *         {@code getParties() - 1} indicates the first
     *         to arrive and zero indicates the last to arrive
     * @throws InterruptedException if the current thread was interrupted
     *         while waiting
     * @throws BrokenBarrierException if <em>another</em> thread was
     *         interrupted or timed out while the current thread was
     *         waiting, or the barrier was reset, or the barrier was
     *         broken when {@code await} was called, or the barrier
     *         action (if present) failed due to an exception
     */
    public int await() throws InterruptedException, BrokenBarrierException {
        try {
            return dowait(false, 0L);
        } catch (TimeoutException toe) {
            throw new Error(toe); // cannot happen
        }
    }

await() 源碼註釋,描述了方法功能:調用該方法的線程進入等待,在CyclicBarrier上進行阻塞等待,直到發生以下情形之一:

  • 在CyclicBarrier上等待的線程數量達到parties,則所有線程被釋放,繼續執行。—— 正常情形
  • 當前線程被中斷,則拋出InterruptedException異常,並停止等待,繼續執行。
  • 其他等待的線程被中斷,則當前線程拋出BrokenBarrierException異常,並停止等待,繼續執行。
  • 其他等待的線程超時,則當前線程拋出BrokenBarrierException異常,並停止等待,繼續執行。
  • 其他線程調用CyclicBarrier.reset()方法,則當前線程拋出BrokenBarrierException異常,並停止等待,繼續執行。

除了第一種屬於正常的情形,其他的都會導致 BrokenBarrierException。
帶時限的await() 會拋出 TimeoutException;

public int await(long timeout, TimeUnit unit) throws InterruptedException,
                                                             BrokenBarrierException,
                                                             TimeoutException

當前線程等待超時,則拋出TimeoutException異常,並停止等待,繼續執行。
當前線程拋出 TimeoutException 異常時,其他線程會拋出 BrokenBarrierException 異常。

await() 和 await(timeout,TimeUnit)帶時限的阻塞等待,總共會有4種情形,產生 BrokenBarrierException;
下面我們一一來看。

Barrier被破壞的4中情形

爲了加深大家對 CyclicBarrier 使用場景的熟悉,我們在復現產生 BrokenBarrierException 的4種情形時,使用運動員比賽的例子:

1.如果有線程已經處於等待狀態,調用reset方法會導致已經在等待的線程出現BrokenBarrierException異常。並且由於出現了BrokenBarrierException,將會導致始終無法等待。

比如,五個運動員,其中一個在等待發令槍的過程中錯誤地接收到裁判傳過來的指令,導致這個運動員以爲今天比賽取消就離開了賽場。但是其他運動員都領會的裁判正確的指令,剩餘的運動員在起跑線上無限地等待下去,並且裁判看到運動員沒有到齊,也不會打發令槍。

class MyThread extends Thread {
    private CyclicBarrier cyclicBarrier;
    private String name;
    private int ID;

    public MyThread(CyclicBarrier cyclicBarrier, String name,int ID) {
        super();
        this.cyclicBarrier = cyclicBarrier;
        this.name = name;
        this.ID=ID;

    }
    @Override
    public void run() {
        System.out.println(name + "開始準備");
        try {
            Thread.sleep(ID*1000);  //不同運動員準備時間不一樣,方便模擬不同情況
            System.out.println(name + "準備完畢!在起跑線等待發令槍");
            try {
                cyclicBarrier.await();
                System.out.println(name + "跑完了路程!");
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
                System.out.println(name+"看不見起跑線了");
            }
            System.out.println(name+"退場!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

}
public class Test {

    public static void main(String[] args) throws InterruptedException {
        CyclicBarrier barrier = new CyclicBarrier(5, new Runnable() {
            @Override
            public void run() {
                System.out.println("發令槍響了,跑!");

            }
        });

        for (int i = 0; i < 5; i++) {
            new MyThread(barrier, "運動員" + i + "號", i).start();
        }
        Thread.sleep(1000);
        barrier.reset();
    }
}

當發生 BrokenBarrierException 時,CyclicBarrier的保障被破壞,不能完成原功能;對應比賽場景,相當於運動員退場了。

運行結果:

運動員0號開始準備
運動員2號開始準備
運動員3號開始準備
運動員0號準備完畢!在起跑線等待發令槍
運動員1號開始準備
運動員4號開始準備
運動員1號準備完畢!在起跑線等待發令槍
運動員0號看不見起跑線了
運動員0號退場!
java.util.concurrent.BrokenBarrierException
	at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
	at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)
	at com.ljheee.juc.BrokenBarrierExceptionDemo$MyThread.run(BrokenBarrierExceptionDemo.java:31)
運動員2號準備完畢!在起跑線等待發令槍
運動員3號準備完畢!在起跑線等待發令槍
運動員4號準備完畢!在起跑線等待發令槍

從輸出可以看到,運動員0號在等待的過程中,主線程調用了reset方法,導致拋出BrokenBarrierException異常。但是其他線程並沒有受到影響,它們會一直等待下去,從而一直被阻塞。
此時程序一直沒停。

這種場景下,因爲有參與者提前離開,導致剩餘參與者永久等待。

2.如果在等待的過程中,線程被中斷,也會拋出BrokenBarrierException異常,並且這個異常會傳播到其他所有的線程。

public class Test {
    static   Map<Integer,Thread>   threads=new HashMap<>();
    public static void main(String[] args) throws InterruptedException {
        CyclicBarrier barrier = new CyclicBarrier(5, new Runnable() {
            @Override
            public void run() {
                System.out.println("發令槍響了,跑!");

            }
        });

        for (int i = 0; i < 5; i++) {
        MyThread t = new MyThread(barrier, "運動員" + i + "號", i);
            threads.put(i, t);
            t.start();
        }
        Thread.sleep(3000);
        threads.get(1).interrupt();
    }
}

運行結果:

運動員0號開始準備
運動員2號開始準備
運動員3號開始準備
運動員1號開始準備
運動員0號準備完畢!在起跑線等待發令槍
運動員4號開始準備
運動員1號準備完畢!在起跑線等待發令槍
運動員2號準備完畢!在起跑線等待發令槍
運動員3號準備完畢!在起跑線等待發令槍
java.lang.InterruptedException
運動員3號看不見起跑線了
運動員3號退場!
運動員2號看不見起跑線了
運動員2號退場!
運動員0號看不見起跑線了
運動員0號退場!
    at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.reportInterruptAfterWait(AbstractQueuedSynchronizer.java:2014)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2048)
    at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:234)
    at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)
    at thread.MyThread.run(MyThread.java:27)
java.util.concurrent.BrokenBarrierException
    at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
    at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)
    at thread.MyThread.run(MyThread.java:27)
java.util.concurrent.BrokenBarrierException
    at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
    at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)
    at thread.MyThread.run(MyThread.java:27)
java.util.concurrent.BrokenBarrierException
    at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
    at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)
    at thread.MyThread.run(MyThread.java:27)
運動員4號準備完畢!在起跑線等待發令槍
java.util.concurrent.BrokenBarrierException
    at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:207)
    at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)
    at thread.MyThread.run(MyThread.java:27)
運動員4號看不見起跑線了
運動員4號退場!

從輸出可以看到,其中一個線程被中斷,那麼所有的運動員都退場了。
在實際使用CyclicBarrier,一定要防止這種情況發生。

3.如果在執行屏障操作過程中發生異常,則該異常將傳播到當前線程中,其他線程會拋出BrokenBarrierException,屏障被損壞。
這個就好比運動員都沒有問題,而是裁判出問題了。裁判權力比較大,直接告訴所有的運動員,今天不比賽了,你們都回家吧!

public class Test {
    static Map<Integer, Thread> threads = new HashMap<>();

    public static void main(String[] args) throws InterruptedException {
        CyclicBarrier barrier = new CyclicBarrier(5, new Runnable() {
            @Override
            public void run() {
                String str = null;
                str.substring(0, 1);// 模擬異常
                System.out.println("發令槍響了,跑!");

            }
        });

        for (int i = 0; i < 5; i++) {
            MyThread t = new MyThread(barrier, "運動員" + i + "號", i);
            threads.put(i, t);
            t.start();
        }

    }
}

運行結果:

運動員0號開始準備
運動員3號開始準備
運動員2號開始準備
運動員1號開始準備
運動員4號開始準備
運動員0號準備完畢!在起跑線等待發令槍
運動員1號準備完畢!在起跑線等待發令槍
運動員2號準備完畢!在起跑線等待發令槍
運動員3號準備完畢!在起跑線等待發令槍
運動員4號準備完畢!在起跑線等待發令槍
Exception in thread "Thread-4" java.util.concurrent.BrokenBarrierException
    at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
    at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)
    at thread.MyThread.run(MyThread.java:27)
運動員0號看不見起跑線了
運動員0號退場!
java.util.concurrent.BrokenBarrierException
    at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
    at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)
    at thread.MyThread.run(MyThread.java:27)
運動員3號看不見起跑線了
運動員3號退場!
java.util.concurrent.BrokenBarrierException
    at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
    at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)
    at thread.MyThread.run(MyThread.java:27)
運動員1號看不見起跑線了
運動員1號退場!
java.lang.NullPointerException
    at thread.Test$1.run(Test.java:15)
    at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:220)
    at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)
    at thread.MyThread.run(MyThread.java:27)
java.util.concurrent.BrokenBarrierException
    at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
    at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)
    at thread.MyThread.run(MyThread.java:27)
運動員2號看不見起跑線了
運動員2號退場!

我們在 CyclicBarrier 的構造方法中指定回調函數,並模擬了異常;
可以看到,如果在執行屏障動作的過程中出現異常,那麼所有的線程都會拋出BrokenBarrierException異常。
這也提醒我們,使用帶回調的CyclicBarrier構造方法時,指定的回調任務一定不要拋出異常,或者實現異常處理。

4.如果超出指定的等待時間,當前線程會拋出 TimeoutException 異常,其他線程會拋出BrokenBarrierException異常。

public class MyThread extends Thread {
    private CyclicBarrier cyclicBarrier;
    private String name;
    private int ID;

    public MyThread(CyclicBarrier cyclicBarrier, String name, int ID) {
        super();
        this.cyclicBarrier = cyclicBarrier;
        this.name = name;
        this.ID = ID;

    }

    @Override
    public void run() {
        System.out.println(name + "開始準備");
        try {
            Thread.sleep(ID * 1000);
            System.out.println(name + "準備完畢!在起跑線等待發令槍");
            try {
                try {
                    cyclicBarrier.await(ID * 1000, TimeUnit.MILLISECONDS);
                } catch (TimeoutException e) {
                    e.printStackTrace();
                }
                System.out.println(name + "跑完了路程!");
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
                System.out.println(name + "看不見起跑線了");
            }
            System.out.println(name + "退場!");
        } catch (InterruptedException e) {

            e.printStackTrace();
        }

    }
}

運行結果:

運動員0號開始準備
運動員3號開始準備
運動員2號開始準備
運動員1號開始準備
運動員0號準備完畢!在起跑線等待發令槍
運動員4號開始準備
運動員0號跑完了路程!
運動員0號退場!
java.util.concurrent.TimeoutException
	at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:257)
	at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
	at com.ljheee.juc.BrokenBarrierExceptionDemo$MyThread.run(BrokenBarrierExceptionDemo.java:34)
運動員1號準備完畢!在起跑線等待發令槍
運動員2號準備完畢!在起跑線等待發令槍
運動員1號跑完了路程!
運動員1號退場!
運動員2號看不見起跑線了
運動員2號退場!
java.util.concurrent.TimeoutException
	at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:257)
	at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
	at com.ljheee.juc.BrokenBarrierExceptionDemo$MyThread.run(BrokenBarrierExceptionDemo.java:34)
java.util.concurrent.BrokenBarrierException
	at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
	at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
	at com.ljheee.juc.BrokenBarrierExceptionDemo$MyThread.run(BrokenBarrierExceptionDemo.java:34)
運動員3號準備完畢!在起跑線等待發令槍
java.util.concurrent.BrokenBarrierException
運動員3號看不見起跑線了
	at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:207)
運動員3號退場!
	at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
	at com.ljheee.juc.BrokenBarrierExceptionDemo$MyThread.run(BrokenBarrierExceptionDemo.java:34)
運動員4號準備完畢!在起跑線等待發令槍
java.util.concurrent.BrokenBarrierException
運動員4號看不見起跑線了
	at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:207)
運動員4號退場!
	at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
	at com.ljheee.juc.BrokenBarrierExceptionDemo$MyThread.run(BrokenBarrierExceptionDemo.java:34)

從輸出可以看到,如果其中一個參與者拋出TimeoutException,其他參與者會拋出 BrokenBarrierException。

如何處理 BrokenBarrierException ?

可以看到,使用 CyclicBarrier 還需注意許多事項,其中 BrokenBarrierException 被稱爲是 CyclicBarrier 的剋星;
那又如何 處理/預防 BrokenBarrierException 呢?

當然,要預防,還需從 CyclicBarrier 的設計開始考慮,設計者已經幫我們考慮了一些問題,如檢查是否被破壞,重置 CyclicBarrier 等。

boolean isBroken():獲取是否破損標誌位broken的值,此值有以下幾種情況:

  • CyclicBarrier初始化時,broken=false,表示屏障未破損。
  • 如果正在等待的線程被中斷,則broken=true,表示屏障破損。
  • 如果正在等待的線程超時,則broken=true,表示屏障破損。
  • 如果有線程調用CyclicBarrier.reset()方法,則broken=false,表示屏障回到未破損狀態。

void reset():使得 CyclicBarrier 迴歸初始狀態,直觀來看它做了兩件事:

  • 如果有正在等待的線程,則會拋出BrokenBarrierException異常,且這些線程停止等待,繼續執行。
  • 將是否破損標誌位broken置爲false。

在任務協同階段,我們可以藉助這兩個API來做輔助;
當然源碼設計者肯定不能從源頭將所有問題都解決,剩下的是需要我們根據業務情況,看是需要終止協作:拋異常、還是直接退出。
並且根據觸發 BrokenBarrierException 的場景,我們在相關代碼實現時,儘量規避。


推薦閱讀

本文首發於 公衆號 架構道與術(ToBeArchitecturer),歡迎關注、學習更多幹貨~

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