面試官:說說CountDownLatch,CyclicBarrier,Semaphore的原理?

科技繆繆發表於2020-10-21

CountDownLatch

CountDownLatch適用於在多執行緒的場景需要等待所有子執行緒全部執行完畢之後再做操作的場景。

舉個例子,早上部門開會,有人在上廁所,這時候需要等待所有人從廁所回來之後才能開始會議。

public class CountDownLatchTest {
    private static int num = 3;
    private static CountDownLatch countDownLatch = new CountDownLatch(num);
    private static ExecutorService executorService = Executors.newFixedThreadPool(num);
    public static void main(String[] args) throws Exception{
        executorService.submit(() -> {
            System.out.println("A在上廁所");
            try {
                Thread.sleep(4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                countDownLatch.countDown();
                System.out.println("A上完了");
            }
        });
        executorService.submit(()->{
            System.out.println("B在上廁所");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                countDownLatch.countDown();
                System.out.println("B上完了");
            }
        });
        executorService.submit(()->{
            System.out.println("C在上廁所");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                countDownLatch.countDown();
                System.out.println("C上完了");
            }
        });

        System.out.println("等待所有人從廁所回來開會...");
        countDownLatch.await();
        System.out.println("所有人都好了,開始開會...");
        executorService.shutdown();

    }
}

 

 

程式碼執行結果:

A在上廁所
B在上廁所
等待所有人從廁所回來開會...
C在上廁所
B上完了
C上完了
A上完了
所有人都好了,開始開會...
 

初始化一個CountDownLatch例項傳參3,因為我們有3個子執行緒,每次子執行緒執行完畢之後呼叫countDown()方法給計數器-1,主執行緒呼叫await()方法後會被阻塞,直到最後計數器變為0,await()方法返回,執行完畢。他和join()方法的區別就是join會阻塞子執行緒直到執行結束,而CountDownLatch可以在任何時候讓await()返回,而且用ExecutorService沒法用join了,相比起來,CountDownLatch更靈活。

CountDownLatch基於AQS實現,volatile變數state維持倒數狀態,多執行緒共享變數可見。

  1. CountDownLatch通過建構函式初始化傳入引數實際為AQS的state變數賦值,維持計數器倒數狀態
  2. 當主執行緒呼叫await()方法時,當前執行緒會被阻塞,當state不為0時進入AQS阻塞佇列等待。
  3. 其他執行緒呼叫countDown()時,state值原子性遞減,當state值為0的時候,喚醒所有呼叫await()方法阻塞的執行緒

CyclicBarrier

CyclicBarrier叫做迴環屏障,它的作用是讓一組執行緒全部達到一個狀態之後再全部同時執行,而且他有一個特點就是所有執行緒執行完畢之後是可以重用的。

public class CyclicBarrierTest {
    private static int num = 3;
    private static CyclicBarrier cyclicBarrier = new CyclicBarrier(num, () -> {
        System.out.println("所有人都好了,開始開會...");
        System.out.println("-------------------");
    });
    private static ExecutorService executorService = Executors.newFixedThreadPool(num);
    public static void main(String[] args) throws Exception{
        executorService.submit(() -> {
            System.out.println("A在上廁所");
            try {
                Thread.sleep(4000);
                System.out.println("A上完了");
                cyclicBarrier.await();
                System.out.println("會議結束,A退出");
            } catch (Exception e) {
                e.printStackTrace();
            }finally {

            }
        });
        executorService.submit(()->{
            System.out.println("B在上廁所");
            try {
                Thread.sleep(2000);
                System.out.println("B上完了");
                cyclicBarrier.await();
                System.out.println("會議結束,B退出");
            } catch (Exception e) {
                e.printStackTrace();
            }finally {

            }
        });
        executorService.submit(()->{
            System.out.println("C在上廁所");
            try {
                Thread.sleep(3000);
                System.out.println("C上完了");
                cyclicBarrier.await();
                System.out.println("會議結束,C退出");
            } catch (Exception e) {
                e.printStackTrace();
            }finally {

            }
        });

        executorService.shutdown();

    }
}
 

輸出結果為:

A在上廁所
B在上廁所
C在上廁所
B上完了
C上完了
A上完了
所有人都好了,開始開會...
-------------------
會議結束,A退出
會議結束,B退出
會議結束,C退出
 

從結果來看和CountDownLatch非常相似,初始化傳入3個執行緒和一個任務,執行緒呼叫await()之後進入阻塞,計數器-1,當計數器為0時,就去執行CyclicBarrier中建構函式的任務,當任務執行完畢後,喚醒所有阻塞中的執行緒。這驗證了CyclicBarrier讓一組執行緒全部達到一個狀態之後再全部同時執行的效果。

再舉個例子來驗證CyclicBarrier可重用的效果。

public class CyclicBarrierTest2 {
    private static int num = 3;
    private static CyclicBarrier cyclicBarrier = new CyclicBarrier(num, () -> {
        System.out.println("-------------------");
    });
    private static ExecutorService executorService = Executors.newFixedThreadPool(num);

    public static void main(String[] args) throws Exception {
        executorService.submit(() -> {
            System.out.println("A在上廁所");
            try {
                Thread.sleep(4000);
                System.out.println("A上完了");
                cyclicBarrier.await();
                System.out.println("會議結束,A退出,開始擼程式碼");
                cyclicBarrier.await();
                System.out.println("C工作結束,下班回家");
                cyclicBarrier.await();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {

            }
        });
        executorService.submit(() -> {
            System.out.println("B在上廁所");
            try {
                Thread.sleep(2000);
                System.out.println("B上完了");
                cyclicBarrier.await();
                System.out.println("會議結束,B退出,開始摸魚");
                cyclicBarrier.await();
                System.out.println("B摸魚結束,下班回家");
                cyclicBarrier.await();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {

            }
        });
        executorService.submit(() -> {
            System.out.println("C在上廁所");
            try {
                Thread.sleep(3000);
                System.out.println("C上完了");
                cyclicBarrier.await();
                System.out.println("會議結束,C退出,開始摸魚");
                cyclicBarrier.await();
                System.out.println("C摸魚結束,下班回家");
                cyclicBarrier.await();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {

            }
        });

        executorService.shutdown();

    }
}
 

輸出結果:

A在上廁所
B在上廁所
C在上廁所
B上完了
C上完了
A上完了
-------------------
會議結束,A退出,開始擼程式碼
會議結束,B退出,開始摸魚
會議結束,C退出,開始摸魚
-------------------
C摸魚結束,下班回家
C工作結束,下班回家
B摸魚結束,下班回家
-------------------
 

從結果來看,每個子執行緒呼叫await()計數器減為0之後才開始繼續一起往下執行,會議結束之後一起進入摸魚狀態,最後一天結束一起下班,這就是可重用

CyclicBarrier還是基於AQS實現的,內部維護parties記錄匯流排程數,count用於計數,最開始count=parties,呼叫await()之後count原子遞減,當count為0之後,再次將parties賦值給count,這就是複用的原理。

  1. 當子執行緒呼叫await()方法時,獲取獨佔鎖,同時對count遞減,進入阻塞佇列,然後釋放鎖
  2. 當第一個執行緒被阻塞同時釋放鎖之後,其他子執行緒競爭獲取鎖,操作同1
  3. 直到最後count為0,執行CyclicBarrier建構函式中的任務,執行完畢之後子執行緒繼續向下執行

Semaphore

Semaphore叫做訊號量,和前面兩個不同的是,他的計數器是遞增的。

public class SemaphoreTest {
    private static int num = 3;
    private static int initNum = 0;
    private static Semaphore semaphore = new Semaphore(initNum);
    private static ExecutorService executorService = Executors.newFixedThreadPool(num);
    public static void main(String[] args) throws Exception{
        executorService.submit(() -> {
            System.out.println("A在上廁所");
            try {
                Thread.sleep(4000);
                semaphore.release();
                System.out.println("A上完了");
            } catch (Exception e) {
                e.printStackTrace();
            }finally {

            }
        });
        executorService.submit(()->{
            System.out.println("B在上廁所");
            try {
                Thread.sleep(2000);
                semaphore.release();
                System.out.println("B上完了");
            } catch (Exception e) {
                e.printStackTrace();
            }finally {

            }
        });
        executorService.submit(()->{
            System.out.println("C在上廁所");
            try {
                Thread.sleep(3000);
                semaphore.release();
                System.out.println("C上完了");
            } catch (Exception e) {
                e.printStackTrace();
            }finally {

            }
        });

        System.out.println("等待所有人從廁所回來開會...");
        semaphore.acquire(num);
        System.out.println("所有人都好了,開始開會...");

        executorService.shutdown();

    }
}
 

輸出結果為:

A在上廁所
B在上廁所
等待所有人從廁所回來開會...
C在上廁所
B上完了
C上完了
A上完了
所有人都好了,開始開會...
 

稍微和前兩個有點區別,建構函式傳入的初始值為0,當子執行緒呼叫release()方法時,計數器遞增,主執行緒acquire()傳參為3則說明主執行緒一直阻塞,直到計數器為3才會返回。

Semaphore還還還是基於AQS實現的,同時獲取訊號量有公平和非公平兩種策略

  1. 主執行緒呼叫acquire()方法時,用當前訊號量值-需要獲取的值,如果小於0,則進入同步阻塞佇列,大於0則通過CAS設定當前訊號量為剩餘值,同時返回剩餘值
  2. 子執行緒呼叫release()給當前訊號量值計數器+1(增加的值數量由傳參決定),同時不停的嘗試因為呼叫acquire()進入阻塞的執行緒

總結

CountDownLatch通過計數器提供了比join更靈活的多執行緒控制方式,CyclicBarrier也可以達到CountDownLatch的效果,而且有可複用的特點,Semaphore則是採用訊號量遞增的方式,開始的時候並不需要關注需要同步的執行緒個數,並且提供獲取訊號的公平和非公平策略。

- END -

 

相關文章