併發工具類(二)同步屏障CyclicBarrier

王小胖醬發表於2019-01-22

前言

  JDK中為了處理執行緒之間的同步問題,除了提供鎖機制之外,還提供了幾個非常有用的併發工具類:CountDownLatch、CyclicBarrier、Semphore、Exchanger、Phaser;
  CountDownLatch、CyclicBarrier、Semphore、Phaser 這四個工具類提供一種併發流程的控制手段;而Exchanger工具類則提供了線上程之間交換資料的一種手段。

簡介

  CyclicBarrier 的字面意思是可迴圈使用(Cyclic)的屏障(Barrier)。它要做的事情是,讓一組執行緒到達一個屏障(也可以叫同步點)時被阻塞,直到最後一個執行緒到達屏障時,屏障才會開門,所有被屏障攔截的執行緒才會繼續幹活。CyclicBarrier預設的構造方法是CyclicBarrier(int parties),其參數列示屏障攔截的執行緒數量,每個執行緒呼叫await方法告訴CyclicBarrier我已經到達了屏障,然後當前執行緒被阻塞。

構造方法摘要

方法名稱說明
CyclicBarrier(int parties)建立一個新的 CyclicBarrier,它將在給定數量的參與者(執行緒)處於等待狀態時啟動,但它不會在啟動 barrier 時執行預定義的操作。
CyclicBarrier(int parties, Runnable barrierAction)建立一個新的 CyclicBarrier,它將在給定數量的參與者(執行緒)處於等待狀態時啟動,並在啟動 barrier 時執行給定的屏障操作,該操作由最後一個進入 barrier 的執行緒執行。

方法摘要

方法名稱說明
public int await() throws
InterruptedException, BrokenBarrierException
在所有參與者都已經在此 barrier 上呼叫 await 方法之前,將一直等待。
返回:
到達的當前執行緒的索引,其中,索引 getParties() - 1 指示將到達的第一個執行緒,
零指示最後一個到達的執行緒.
public int await(long timeout,TimeUnit unit) throws
InterruptedException,BrokenBarrierException,
ITimeoutException
在所有參與者都已經在此屏障上呼叫 await 方法之前將一直等待,或者超出了指定的等待時間。
public void reset()將屏障重置為其初始狀態。如果所有參與者目前都在屏障處等待,則它們將返回,同時丟擲一個 BrokenBarrierException。注意,在由於其他原因造成損壞之後,實行重置可能會變得很複雜;
public boolean isBroken()查詢此屏障是否處於損壞狀態。
public int getNumberWaiting()返回當前在屏障處等待的參與者數目。此方法主要用於除錯和斷言。
public int getParties()返回要求啟動此 barrier 的參與者數目。

注意:

  • 對於失敗的同步嘗試,CyclicBarrier 使用了一種要麼全部要麼全不 (all-or-none) 的破壞模式:
    如果因為中斷、失敗或者超時等原因,導致執行緒過早地離開了屏障點,那麼在該屏障點等待的其他所有執行緒也將通過 BrokenBarrierException(如果它們幾乎同時被中斷,則用 InterruptedException)以反常的方式離開。
  • 記憶體一致性效果:
    執行緒中呼叫 await() 之前的操作 happen-before 那些是屏障操作的一部份的操作,後者依次 happen-before 緊跟在從另一個執行緒中對應 await() 成功返回的操作。
@ Example1 屏障操作的例子

public static void main(String[] args) {
    //設定5個屏障,並且有屏障操作
    CyclicBarrier barrier = new CyclicBarrier(5,new Runnable() {
        @Override
        public void run() {
                System.out.println("執行緒"+Thread.currentThread().getName()+"執行了屏障操作");        
        }
    });
    
    for(int i=0;i<5;i++){
       //建立5個執行緒
        Thread thread = new Thread(new MyRunable(barrier),"thread_"+i);
        thread.start();
    }
}
複製程式碼

class MyRunable implements Runnable{

    CyclicBarrier barrier;
    public MyRunable(CyclicBarrier barrier ){
         this.barrier = barrier; 
    }
    
    @Override
    public void run() {
        //一系列操作...
         System.out.println("執行緒 "+Thread.currentThread().getName()+" 到達了屏障點!");
         try {
             int index = barrier.await();
            if(index== (barrier.getParties()-1)){
                //第一個到達屏障點的執行緒,執行特殊操作....
                System.out.println("所有執行緒到達屏障點,執行緒 "+Thread.currentThread().getName()+" 被喚醒!!此執行緒是第一個到達屏障點");
            }else if(index == 0){//最後一個到達屏障點的執行緒
                System.out.println("所有執行緒到達屏障點,執行緒 "+Thread.currentThread().getName()+" 被喚醒!!此執行緒是最後一個到達屏障點");
            }else{
                System.out.println("所有執行緒到達屏障點,執行緒 "+Thread.currentThread().getName()+" 被喚醒!!");
            }
        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }
    }
}
複製程式碼

執行結果:

執行緒 thread_1 到達了屏障點!
執行緒 thread_4 到達了屏障點!
執行緒 thread_3 到達了屏障點!
執行緒 thread_0 到達了屏障點!
執行緒 thread_2 到達了屏障點!
執行緒thread_3執行了屏障操作
所有執行緒到達屏障點,執行緒 thread_3 被喚醒!!此執行緒是最後一個到達屏障點
所有執行緒到達屏障點,執行緒 thread_0 被喚醒!!
所有執行緒到達屏障點,執行緒 thread_4 被喚醒!!
所有執行緒到達屏障點,執行緒 thread_1 被喚醒!!此執行緒是第一個到達屏障點
所有執行緒到達屏障點,執行緒 thread_2 被喚醒!!
複製程式碼

 上面的例子,使用了傳入屏障操作的Runable引數的構造方法,

屏障操作是由最後一個到達屏障點的執行緒執行的,這是不可以改變的

。然而,在實際使用中,

可能會出現由第n個到達屏障點的執行緒執行特殊的操作(或者說 屏障操作),那麼就可以使用 CyclicBarrier.await()進行判斷

,如上面的例子,第一個和最後一個到達屏障點的執行緒都執行特殊的操作。

   順便說一下,可能會對本例子中前5個輸出的順序 有所疑惑:thread_3 通過awiat()方法返回的索引值,可知 thread_3 是最後一個到達屏障點的,但為什麼輸出的順序卻是第三個,而不是最後一個;在這就要真正理解CyclicBarrier,CyclicBarrier 本質上是一把鎖,多個執行緒在使用CyclicBarrier 物件時,是需要先獲取鎖,即需要互斥訪問,所以呼叫await( )方法不一定能夠馬上獲取鎖。上面的例子,是先列印輸出,再去獲取鎖,所以輸出順序不是到達屏障點的順序。

@ Example2 應用場景

   下面的例子是:CyclicBarrier用於多執行緒計算資料,最後合併計算結果的場景。比如我們用一個Excel儲存了使用者所有銀行流水,每個Sheet儲存一個帳戶近一年的每筆銀行流水,現在需要統計使用者的日均銀行流水,先用多執行緒處理每個sheet裡的銀行流水,都執行完之後,得到每個sheet的日均銀行流水,最後,再用barrierAction用這些執行緒的計算結果,計算出整個Excel的日均銀行流水。

public class BankWaterService implements Runnable {
    
    //建立4個屏障,處理完後執行當前類的run方法
    private CyclicBarrier barrier = new CyclicBarrier(4,this);
    
    //假設只有4個sheet,所以只啟動4個執行緒
    private Executor excutor = Executors.newFixedThreadPool(4);
    
    //儲存每個sheet計算出的結果
    private ConcurrentHashMap< String, Integer> sheetBankWaterCount = new ConcurrentHashMap<>();
    
    private void count(){
        for(int i=0;i<4;i++){
            excutor.execute(new Runnable() {
                
                @Override
                public void run() {
                    //計算過程.....
                    //儲存計算結果
                    sheetBankWaterCount.put(Thread.currentThread().getName(), 1);
                    try {
                        //計算完成,插入屏障
                        barrier.await();
                        //後續操作,將會使用到四個執行緒的執行結果....
                        System.out.println("執行緒"+Thread.currentThread().getName()+"執行結束,最終的計算結果:"+sheetBankWaterCount.get("result"));
                    } catch (InterruptedException | BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
    

    @Override
    public void run() {
        int result = 0;
        for(Entry<String, Integer> item : sheetBankWaterCount.entrySet()){
            result += item.getValue();
        }
        sheetBankWaterCount.put("result", result);
    }
    
    public static void main(String[] args) {
        BankWaterService bankWaterService = new BankWaterService();
        bankWaterService.count();
    }
}
複製程式碼

執行結果:

執行緒pool-1-thread-4執行結束,最終的計算結果:4
執行緒pool-1-thread-2執行結束,最終的計算結果:4
執行緒pool-1-thread-1執行結束,最終的計算結果:4
執行緒pool-1-thread-3執行結束,最終的計算結果:4
複製程式碼

CyclicBarrier和CountDownLatch的區別

  • CountDownLatch: 一個執行緒(或者多個執行緒), 等待另外N個執行緒完成某個事情之後才能執行。而這N個執行緒通過呼叫CountDownLatch.countDown()方法 來告知“某件事件”完成,即計數減一。而一個執行緒(或者多個執行緒)則通過CountDownLatch.awiat( ) 進入等待狀態,直到 CountDownLatch的計數為0時,才會全部被喚醒
  • CyclicBarrier : N個執行緒相互等待,任何一個執行緒完成某個事情之前,所有的執行緒都必須等待。
    CountDownLatch 是計數器, 執行緒完成一個就記一個, 就像 報數一樣, 只不過是遞減的.
    而CyclicBarrier更像一個水閘, 執行緒執行就想水流, 在水閘處都會堵住, 等到水滿(執行緒到齊)了, 才開始洩流.
  • CountDownLatch只能使用一次,CyclicBarrier則可以通過reset( )方法重置後,重新使用。所以
    CyclicBarrier可以用於更復雜的業務場景。
    例如:計算錯誤,可以重置計數器,並讓執行緒重新執行一次。

文章源地址:https://www.cnblogs.com/jinggod/p/8494193.html


相關文章