Java併發之CountDownLatch、CyclicBarrier和Semaphore

sunjiaminaini發表於2017-08-10

JUC 中的同步器三個主要的成員:CountDownLatch、CyclicBarrier 和 Semaphore(不知道有沒有初學者覺得這三個的名字不太好記)。這三個是 JUC 中較為常用的同步器,通過它們可以方便地實現很多執行緒之間協作的功能。
CountDownLatch
直譯過來就是倒計數(CountDown)門閂(Latch)。倒計數不用說,門閂的意思顧名思義就是阻止前進。在這裡就是指 CountDownLatch.await() 方法在倒計數為0之前會阻塞當前執行緒。
作用
CountDownLatch 的作用和 Thread.join() 方法類似,可用於一組執行緒和另外一組執行緒的協作。例如,主執行緒在做一項工作之前需要一系列的準備工作,只有這些準備工作都完成,主執行緒才能繼續它的工作。這些準備工作彼此獨立,所以可以併發執行以提高速度。在這個場景下就可以使用 CountDownLatch 協調執行緒之間的排程了。在直接建立執行緒的年代(Java 5.0 之前),我們可以使用 Thread.join()。在 JUC 出現後,因為執行緒池中的執行緒不能直接被引用,所以就必須使用 CountDownLatch 了。
示例
下面的這個例子可以理解為 F1 賽車的維修過程,只有 startSignal (可以表示停車,可能名字不太貼合)命令下達之後,維修工才開始幹活,只有等所有工人完成工作之後,賽車才能繼續。

 class Driver { // ...  
     void main() throws InterruptedException {  
         CountDownLatch startSignal = new CountDownLatch(1);  
         CountDownLatch doneSignal = new CountDownLatch(N);  

         for (int i = 0; i < N; ++i) // create and start threads  
              new Thread(new Worker(startSignal, doneSignal)).start();  

       doSomethingElse();            // don't let run yet  
         startSignal.countDown();      // let all threads proceed  
         doSomethingElse();  
        doneSignal.await();           // wait for all to finish  
      }  
 }  

 class Worker implements Runnable {  
     private final CountDownLatch startSignal;  
     private final CountDownLatch doneSignal;  
    Worker(CountDownLatch startSignal, CountDownLatch doneSignal) {  
       this.startSignal = startSignal;  
         this.doneSignal = doneSignal;  
     }  
     public void run() {  
         try {  
             startSignal.await();  
             doWork();  
             doneSignal.countDown();  
         } catch (InterruptedException ex) {} // return;  
    }  

      void doWork() { ... }  
   } 

當 startSignal.await() 會阻塞執行緒,當 startSignal.countDown() 被呼叫之後,所有 Worker 執行緒開始執行 doWork() 方法,所以 Worker。doWork() 是幾乎同時開始執行的。當 Worker.doWork() 執行完畢後,呼叫 doneSignal.countDown(),在所有 Worker 執行緒執行完畢之後,主執行緒繼續執行。
CyclicBarrier
CyclicBarrier 翻譯過來叫迴圈柵欄、迴圈障礙什麼的(還是有點彆扭的。所以還是別翻譯了,只可意會不可言傳啊)。它主要的方法就是一個:await()。await() 方法每被呼叫一次,計數便會減少1,並阻塞住當前執行緒。當計數減至0時,阻塞解除,所有在此 CyclicBarrier 上面阻塞的執行緒開始執行。在這之後,如果再次呼叫 await() 方法,計數就又會變成 N-1,新一輪重新開始,這便是 Cyclic 的含義所在。
CyclicBarrier 的使用並不難,但需要注意它所相關的異常。除了常見的異常,CyclicBarrier.await() 方法會丟擲一個獨有的 BrokenBarrierException。這個異常發生在當某個執行緒在等待本 CyclicBarrier 時被中斷或超時或被重置時,其它同樣在這個 CyclicBarrier 上等待的執行緒便會受到 BrokenBarrierException。意思就是說,同志們,別等了,有個小夥伴已經掛了,我們們如果繼續等有可能會一直等下去,所有各回各家吧。
CyclicBarrier.await() 方法帶有返回值,用來表示當前執行緒是第幾個到達這個 Barrier 的執行緒。
和 CountDownLatch 一樣,CyclicBarrier 同樣可以可以在建構函式中設定總計數值。與 CountDownLatch 不同的是,CyclicBarrier 的建構函式還可以接受一個 Runnable,會在 CyclicBarrier 被釋放時執行。
NOTE: CyclicBarrier 的功能也可以由 CountDownLatch 來實現
示例
CyclicBarrier 的應用(當然,這個例子換成 CountDownLatch 也是可以實現的,很簡單,就不說怎麼寫了)

class Solver {  
    final int N;  
   final float[][] data;  
    final CyclicBarrier barrier;  

    class Worker implements Runnable {  
        int myRow;  
        Worker(int row) { myRow = row; }  
      public void run() {  
          while (!done()) {  
                processRow(myRow);  

                try {  
                    barrier.await();  
                } catch (InterruptedException ex) {  
                    return;  
                } catch (BrokenBarrierException ex) {  
                    return;  
                }  
            }  
        }  
    }  

    public Solver(float[][] matrix) {  
       data = matrix;  
      N = matrix.length;  
       barrier = new CyclicBarrier(N, new Runnable() {  
               public void run() {  
                    mergeRows(...);  
                }  
            });  
        for (int i = 0; i < N; ++i)  
            new Thread(new Worker(i)).start();  

        waitUntilDone();  
    }  
} 

CyclicBarrier 和 CountDownLatch 在用法上的不同
CountDownLatch 適用於一組執行緒和另一個主執行緒之間的工作協作。一個主執行緒等待一組工作執行緒的任務完畢才繼續它的執行是使用 CountDownLatch 的主要場景;CyclicBarrier 用於一組或幾組執行緒,比如一組執行緒需要在一個時間點上達成一致,例如同時開始一個工作。另外,CyclicBarrier 的迴圈特性和建構函式所接受的 Runnable 引數也是 CountDownLatch 所不具備的。
Semaphore
Semaphore 直譯是訊號量,可能稱它是許可量更容易理解。當然,因為在電腦科學中這個名字由來已久,所以不能亂改。它的功能比較好理解,就是通過建構函式設定一個數量的許可,然後通過 acquire 方法獲得許可,release方法釋放許可。它還有 tryAcquire 和 acquireUninterruptibly 方法,可以根據自己的需要選擇
示例:Semaphore 控制資源訪問

public class SemaphoreWorkThread implements Runnable {
    Semaphore semaphore;
    String threadName;

    public SemaphoreWorkThread(Semaphore semaphore, String threadName) {
        this.semaphore = semaphore;
        this.threadName = threadName;
    }

    public void run() {

        try {
            semaphore.acquire();
            System.out.println("Id :"+threadName+" 開始工作");
            Thread.sleep(3000);
            System.out.println("Id :"+threadName+" 工作結束");
            semaphore.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }


    }
}

public class SemaphoreTest {

    public static void main(String[] args) {
        System.out.println("SemaphoreTest.main");
        Semaphore cb = new Semaphore(2);
        System.out.println("Main Thread 開始工作...");
        ExecutorService executorService = Executors.newScheduledThreadPool(10);
        for (int i=0;i<100;i++){
            executorService.submit(new SemaphoreWorkThread(cb,""+i));
        }
        System.out.println("Main thread 結束工作... ");
    }
}

上面這個示例中 Semaphore 的用法沒什麼可多講的。需要留言的是這裡面有兩個同步方法,不過對吞吐應該沒什麼影響,因為主要是對一個 boolean 陣列做一下 O(n) 的操作,而且每個迴圈裡面的操作很簡單,所以速度很快。不過不知道 JUC 裡面執行緒池的控制是怎麼做的,本人不才,還沒看過那塊原始碼,得空看看,有知道的也可以說說。
最後一句話總結
CountDownLatch 是能使一組執行緒等另一組執行緒都跑完了再繼續跑;CyclicBarrier 能夠使一組執行緒在一個時間點上達到同步,可以是一起開始執行全部任務或者一部分任務。同時,它是可以迴圈使用的;Semaphore 是隻允許一定數量的執行緒同時執行一段任務。

文章內容來自網上資源,只為記錄相關知識,如有侵權,請聯絡作者。

相關文章