Java多線程9 同步工具類CyclicBarrier

Java多線程目錄
CyclicBarrier是一個同步工具類,它允許一組線程互相等待,直到達到某個公共屏障點。與CountDownLatch不同的是該barrier在釋放線程等待後可以重用,所以它稱爲循環(Cyclic)的屏障(Barrier)。
CyclicBarrier支持一個可選的Runnable命令,在一組線程中的最後一個線程到達之後(但在釋放所有線程之前),該命令只在每個屏障點運行一次。若再繼續所有的參與線程之前更新共享狀態,此屏蔽操作很有用。

1 CyclicBarrier方法說明

CyclicBarrier提供的方法有:

CyclicBarrier(parties):初始化相互等待的線程數量的構造方法。

CyclicBarrier(parties,Runnable barrierAction):初始化相互等待的線程數量以及屏障線程的構造方法。
屏障線程的運行時機:
等待的線程數量=parties之後,CyclicBarrier打開屏障之前。
舉例:在分組計算中,每個線程負責一部分計算,最終這些線程計算結束之後,交由屏障線程進行彙總計算。

int getParties():獲取CyclicBarrier打開屏障的線程數量,也成爲方數。

int getNumberWaiting():獲取正在CyclicBarrier上等待的線程數量。

int await():在CyclicBarrier上進行阻塞等待,直到發生以下情形之一:

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

int await(timeout,TimeUnit):在CyclicBarrier上進行限時的阻塞等待,直到發生以下情形之一:

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

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

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

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

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

2 CyclicBarrier實例

假若有若干個線程都要進行寫數據操作,並且只有所有線程都完成寫數據操作之後,這些線程才能繼續做後面的事情,此時就可以利用CyclicBarrier了:

 public static void main(String[] args) {
        int N = 4;
        CyclicBarrier barrier  = new CyclicBarrier(N);
        for(int i=0;i<N;i++)
            new Writer(barrier).start();
    }
    static class Writer extends Thread{
        private CyclicBarrier cyclicBarrier;
        public Writer(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }
 
        @Override
        public void run() {
            System.out.println("線程"+Thread.currentThread().getName()+"正在寫入數據...");
            try {
                Thread.sleep(5000);      //以睡眠來模擬寫入數據操作
                System.out.println("線程"+Thread.currentThread().getName()+"寫入數據完畢,等待其他線程寫入完畢");
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }catch(BrokenBarrierException e){
                e.printStackTrace();
            }
            System.out.println("所有線程寫入完畢,繼續處理其他任務...");
        }
    }
線程Thread-0正在寫入數據...
線程Thread-3正在寫入數據...
線程Thread-1正在寫入數據...
線程Thread-2正在寫入數據...
線程Thread-1寫入數據完畢,等待其他線程寫入完畢
線程Thread-3寫入數據完畢,等待其他線程寫入完畢
線程Thread-2寫入數據完畢,等待其他線程寫入完畢
線程Thread-0寫入數據完畢,等待其他線程寫入完畢
所有線程寫入完畢,繼續處理其他任務...
所有線程寫入完畢,繼續處理其他任務...
所有線程寫入完畢,繼續處理其他任務...
所有線程寫入完畢,繼續處理其他任務...

從上面輸出結果可以看出,每個寫入線程執行完寫數據操作之後,就在等待其他線程寫入操作完畢。

當所有線程線程寫入操作完畢之後,所有線程就繼續進行後續的操作了。

如果想在所有線程寫入操作完之後,進行額外的其他操作可以爲CyclicBarrier提供Runnable參數:
public class CyclicBarrierTest {

    public static void main(String[] args) {
        int N = 4;
        CyclicBarrier barrier  = new CyclicBarrier(N,new Runnable() {
            @Override
            public void run() {
                System.out.println("當前線程"+Thread.currentThread().getName());
            }
        });

        for(int i=0;i<N;i++)
            new Writer(barrier).start();
    }
    static class Writer extends Thread{
        private CyclicBarrier cyclicBarrier;
        public Writer(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }

        @Override
        public void run() {
            System.out.println("線程"+Thread.currentThread().getName()+"正在寫入數據...");
            try {
                Thread.sleep(3000);      //以睡眠來模擬寫入數據操作
                System.out.println("線程"+Thread.currentThread().getName()+"寫入數據完畢,等待其他線程寫入完畢");
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }catch(BrokenBarrierException e){
                e.printStackTrace();
            }
            System.out.println("所有線程寫入完畢,繼續處理其他任務...");
        }
    }

}
線程Thread-0正在寫入數據...
線程Thread-3正在寫入數據...
線程Thread-2正在寫入數據...
線程Thread-1正在寫入數據...
線程Thread-1寫入數據完畢,等待其他線程寫入完畢
線程Thread-3寫入數據完畢,等待其他線程寫入完畢
線程Thread-0寫入數據完畢,等待其他線程寫入完畢
線程Thread-2寫入數據完畢,等待其他線程寫入完畢
當前線程Thread-2
所有線程寫入完畢,繼續處理其他任務...
所有線程寫入完畢,繼續處理其他任務...
所有線程寫入完畢,繼續處理其他任務...
所有線程寫入完畢,繼續處理其他任務...

從結果可以看出,當四個線程都到達barrier狀態後,會從四個線程中選擇一個線程去執行Runnable。

await指定時間的效果:
public class CyclicBarrierTest {


    public static void main(String[] args) {
        int N = 4;
        CyclicBarrier barrier = new CyclicBarrier(N);

        for (int i = 0; i < N; i++) {
            if (i < N - 1)
                new Writer(barrier).start();
            else {
                try {
                    //運行時間遠小於2000(cyclicBarrier.await 指定時間) 就不會拋出TimeoutException
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                new Writer(barrier).start();
            }
            
        }
    }

    static class Writer extends Thread {
        private CyclicBarrier cyclicBarrier;

        public Writer(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }

        @Override
        public void run() {
            System.out.println("線程" + Thread.currentThread().getName() + "正在寫入數據...");
            try {
                Thread.sleep(3000);      //以睡眠來模擬寫入數據操作
                System.out.println("線程" + Thread.currentThread().getName() + "寫入數據完畢,等待其他線程寫入完畢");
                try {
                    cyclicBarrier.await(2000, TimeUnit.MILLISECONDS);
                } catch (TimeoutException e) {
                    e.printStackTrace();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "所有線程寫入完畢,繼續處理其他任務...");
        }
    }
}
線程Thread-0正在寫入數據...
線程Thread-2正在寫入數據...
線程Thread-1正在寫入數據...
線程Thread-0寫入數據完畢,等待其他線程寫入完畢
線程Thread-2寫入數據完畢,等待其他線程寫入完畢
線程Thread-1寫入數據完畢,等待其他線程寫入完畢
線程Thread-3正在寫入數據...
java.util.concurrent.TimeoutException
    at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:257)
    at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
    at CyclicBarrierTest$Writer.run(CyclicBarrierTest.java:43)
Thread-0所有線程寫入完畢,繼續處理其他任務...
java.util.concurrent.BrokenBarrierException
    at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
    at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
    at CyclicBarrierTest$Writer.run(CyclicBarrierTest.java:43)
Thread-1所有線程寫入完畢,繼續處理其他任務...
java.util.concurrent.BrokenBarrierException
    at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
    at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
    at CyclicBarrierTest$Writer.run(CyclicBarrierTest.java:43)
Thread-2所有線程寫入完畢,繼續處理其他任務...
線程Thread-3寫入數據完畢,等待其他線程寫入完畢
java.util.concurrent.BrokenBarrierException
    at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:207)
    at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
    at CyclicBarrierTest$Writer.run(CyclicBarrierTest.java:43)
Thread-3所有線程寫入完畢,繼續處理其他任務...

上面的代碼在main方法的for循環中,故意讓最後一個線程啓動延遲,因爲在前面三個線程都達到barrier之後,等待了指定的時間發現第四個線程還沒有達到barrier,就拋出異常並繼續執行後面的任務。

另外CyclicBarrier是可以重用的,看下面這個例子:
public class CyclicBarrierTest {

    public static void main(String[] args) {
        int N = 4;
        CyclicBarrier barrier  = new CyclicBarrier(N);

        for(int i=0;i<N;i++) {
            new Writer(barrier).start();
        }

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("CyclicBarrier重用");

        for(int i=0;i<N;i++) {
            new Writer(barrier).start();
        }
    }
    static class Writer extends Thread{
        private CyclicBarrier cyclicBarrier;
        public Writer(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }

        @Override
        public void run() {
            System.out.println("線程"+Thread.currentThread().getName()+"正在寫入數據...");
            try {
                Thread.sleep(3000);      //以睡眠來模擬寫入數據操作
                System.out.println("線程"+Thread.currentThread().getName()+"寫入數據完畢,等待其他線程寫入完畢");

                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }catch(BrokenBarrierException e){
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"所有線程寫入完畢,繼續處理其他任務...");
        }
    }
}
線程Thread-0正在寫入數據...
線程Thread-3正在寫入數據...
線程Thread-2正在寫入數據...
線程Thread-1正在寫入數據...
線程Thread-1寫入數據完畢,等待其他線程寫入完畢
線程Thread-0寫入數據完畢,等待其他線程寫入完畢
線程Thread-3寫入數據完畢,等待其他線程寫入完畢
線程Thread-2寫入數據完畢,等待其他線程寫入完畢
Thread-2所有線程寫入完畢,繼續處理其他任務...
Thread-1所有線程寫入完畢,繼續處理其他任務...
Thread-3所有線程寫入完畢,繼續處理其他任務...
Thread-0所有線程寫入完畢,繼續處理其他任務...
CyclicBarrier重用
線程Thread-4正在寫入數據...
線程Thread-5正在寫入數據...
線程Thread-6正在寫入數據...
線程Thread-7正在寫入數據...
線程Thread-5寫入數據完畢,等待其他線程寫入完畢
線程Thread-4寫入數據完畢,等待其他線程寫入完畢
線程Thread-7寫入數據完畢,等待其他線程寫入完畢
線程Thread-6寫入數據完畢,等待其他線程寫入完畢
Thread-6所有線程寫入完畢,繼續處理其他任務...
Thread-5所有線程寫入完畢,繼續處理其他任務...
Thread-4所有線程寫入完畢,繼續處理其他任務...
Thread-7所有線程寫入完畢,繼續處理其他任務...

從執行結果可以看出,在初次的4個線程越過barrier狀態後,又可以用來進行新一輪的使用。而CountDownLatch無法進行重複使用。

3 CyclicBarrier源碼解析
先看一下CyclicBarrier中成員變量的組成:

    /** The lock for guarding barrier entry */
    private final ReentrantLock lock = new ReentrantLock();
    /** Condition to wait on until tripped */
    private final Condition trip = lock.newCondition();
    /** The number of parties */
    private final int parties;//攔截的線程數量
    /* The command to run when tripped */
    private final Runnable barrierCommand; //當屏障撤銷時,需要執行的屏障操作
    //當前的Generation。每當屏障失效或者開閘之後都會自動替換掉。從而實現重置的功能。
    private Generation generation = new Generation();

    /**
     * Number of parties still waiting. Counts down from parties to 0
     * on each generation.  It is reset to parties on each new
     * generation or when broken.
     */
    private int count;

可以看出,CyclicBarrier是由ReentrantLock和Condition來實現的。具體每個變量都有什麼意義,我們在分析源碼的時候具體說。
我們主要從CyclicBarrier的構造方法和它的await方法分析說起。

CyclicBarrier構造函數

CyclicBarrier有兩個構造函數:

//帶Runnable參數的函數
 public CyclicBarrier(int parties, Runnable barrierAction) {
        if (parties <= 0) throw new IllegalArgumentException();
        this.parties = parties;//有幾個運動員要參賽
        this.count = parties;//目前還需要幾個運動員準備好
        //你要在所有線程都繼續執行下去之前要執行什麼操作,可以爲空
        this.barrierCommand = barrierAction;
    }
//不帶Runnable參數的函數
 public CyclicBarrier(int parties) {
     this(parties, null);
 }

其中,第二個構造函數調用的是第一個構造函數,這個 Runnable barrierAction 參數是什麼呢?其實在上面的小示例中我們就用到了這個Runnable參數,它就是在所有線程都準備好之後,滿足Barrier條件時,並且在所有線程繼續執行之前,我們可以執行這個Runnable。但是值得注意的是,這不是新起了一個線程,而是通過最後一個準備好的(也就是最後一個到達Barrier的)線程承擔啓動的。這一點我們在上面示例中打印的運行結果中也可以看出來:Thread-2線程是最後一個準備好的,就是它執行的這個barrierAction。
這裏parties和count不要混淆,parties是表示必須有幾個線程要到達Barrier,而count是表示目前還有幾個線程未到達Barrier。也就是說,只有當count參數爲0時,Barrier條件即滿足,所有線程可以繼續執行。
count變量是怎麼減少到0的呢?是通過Barrier執行的await方法。下面我們就看一下await方法。

await方法
    public int await() throws InterruptedException, BrokenBarrierException {
        try {
            return dowait(false, 0L);
        } catch (TimeoutException toe) {
            throw new Error(toe); // cannot happen
        }
    }
await方法調用的dowait方法:
    private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
        final ReentrantLock lock = this.lock;
        lock.lock();//獲取ReentrantLock互斥鎖
        try {
            final Generation g = generation;//獲取generation對象

            if (g.broken)//如果generation損壞,拋出異常
                throw new BrokenBarrierException();

            if (Thread.interrupted()) {
                //如果當前線程被中斷,則調用breakBarrier方法,停止CyclicBarrier,並喚醒所有線程
                breakBarrier();
                throw new InterruptedException();
            }

            int index = --count;// 看到這裏了吧,count減1 
            //index=0,也就是說,有0個線程未滿足CyclicBarrier條件,也就是條件滿足,
            //可以喚醒所有的線程了
            if (index == 0) {  // tripped
                boolean ranAction = false;
                try {
                   //這就是構造器的第二個參數,如果不爲空的話,就執行這個Runnable的run方法,
                   //你看,這裏是執行的是run方法,也就是說,並沒有新起一個另外的線程,
                   //而是最後一個執行await操作的線程執行的這個run方法。
                    final Runnable command = barrierCommand;
                    if (command != null)
                        command.run(); //同步執行barrierCommand
                    ranAction = true;
                    nextGeneration(); //執行成功設置下一個nextGeneration
                    return 0;
                } finally {
                    if (!ranAction) . //如果barrierCommand執行失敗,進行屏障破壞處理
                        breakBarrier();
                }
            }
            //如果當前線程不是最後一個到達的線程
            // loop until tripped, broken, interrupted, or timed out
            for (;;) {
                try {
                    if (!timed)
                        trip.await(); //調用Condition的await()方法阻塞
                    else if (nanos > 0L)
                        nanos = trip.awaitNanos(nanos); //調用Condition的awaitNanos()方法阻塞
                } catch (InterruptedException ie) {
                //如果當前線程被中斷,則判斷是否有其他線程已經使屏障破壞。若沒有則進行屏障破壞處理,並拋出異常;否則再次中斷當前線程
                    if (g == generation && ! g.broken) {
                        breakBarrier();//執行breakBarrier,喚醒所有線程
                        throw ie;
                    } else {
                        // We're about to finish waiting even if we had not
                        // been interrupted, so this interrupt is deemed to
                        // "belong" to subsequent execution.
                        Thread.currentThread().interrupt();
                    }
                }

                if (g.broken)//如果當前generation已經損壞,拋出異常
                    throw new BrokenBarrierException();

                if (g != generation)//如果generation已經更新換代,則返回index
                    return index;
                //如果是參數是超時等待,並且已經超時,則執行breakBarrier()方法
                //喚醒所有等待線程。
                if (timed && nanos <= 0L) {
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
            lock.unlock();
        }
    }

簡單來說,如果不發生異常,線程不被中斷,那麼dowait方法會調用Condition的await方法(具體Condition的原理請看前面的文章),直到所有線程都準備好,即都執行了dowait方法,(做count的減操作,直到count=0),即CyclicBarrier條件已滿足,就會執行喚醒線程操作,也就是上面的nextGeneration()方法。可能大家會有疑惑,這個Generation是什麼東西呢?其實這個Generation定義的很簡單,就一個布爾值的成員變量:

private Generation generation = new Generation();

private static class Generation {
    boolean broken = false;
}

Generation 可以理解成“代”,我們要知道,CyclicBarrier是可以重複使用的,CyclicBarrier中的同一批線程屬於同一“代”,當所有線程都滿足了CyclicBarrier條件,執行喚醒操作nextGeneration()方法時,會新new 出一個Generation,代表一下“代”。

nextGeneration的源碼
    private void nextGeneration() {
        // signal completion of last generation
        trip.signalAll();//調用Condition的signalAll方法,喚醒所有await的線程
        // set up next generation
        count = parties;//重置count值
        //生成新的Generation,表示上一代的所有線程已經喚醒,進行更新換代
        generation = new Generation(); 
    }
breakBarrier源碼

再來看一下breakBarrier的代碼,breakBarrier方法是在當前線程被中斷時執行的,用來喚醒所有的等待線程:

    private void breakBarrier() {
        generation.broken = true;//表示當代因爲線程被中斷,已經發成損壞了
        count = parties;//重置count值
        trip.signalAll();//調用Condition的signalAll方法,喚醒所有await的線程
    }
isBroken方法
    public boolean isBroken() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return generation.broken;
        } finally {
            lock.unlock();
        }
    }

判斷此屏障是否處於中斷狀態。如果因爲構造或最後一次重置而導致中斷或超時,從而使一個或多個參與者擺脫此屏障點,或者因爲異常而導致某個屏障操作失敗,則返回true;否則返回false。

reset方法
    //將屏障重置爲其初始狀態。
    public void reset() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            //喚醒所有等待的線程繼續執行,並設置屏障中斷狀態爲true
            breakBarrier();   // break the current generation
            //喚醒所有等待的線程繼續執行,並設置屏障中斷狀態爲false
            nextGeneration(); // start a new generation
        } finally {
            lock.unlock();
        }
    }
getNumberWaiting方法
    //返回當前在屏障處等待的參與者數目,此方法主要用於調試和斷言。
    public int getNumberWaiting() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return parties - count;
        } finally {
            lock.unlock();
        }
    }

總結:
1.CyclicBarrier可以用於多線程計算數據,最後合併計算結果的應用場景。
2.這個等待的await方法,其實是使用ReentrantLock和Condition控制實現的。
3.CyclicBarrier可以重複使用。

特別感謝

CyclicBarrier的原理分析和使用
Java併發編程:CountDownLatch、CyclicBarrier和Semaphore

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