CountDownLatch的兩種常用場景

CountDownLatch的兩種使用場景

先來看看 CountDownLatch 的源碼註釋;

/**
 * A synchronization aid that allows one or more threads to wait until
 * a set of operations being performed in other threads completes.
 *
 * @since 1.5
 * @author Doug Lea
 */
public class CountDownLatch {
}

描述如下:它是一個同步工具類,允許一個或多個線程一直等待,直到其他線程運行完成後再執行。

通過描述,可以清晰的看出,CountDownLatch的兩種使用場景:

  • 場景1:讓多個線程等待
  • 場景2:和讓單個線程等待。
場景1 讓多個線程等待:模擬併發,讓併發線程一起執行

爲了模擬高併發,讓一組線程在指定時刻(秒殺時間)執行搶購,這些線程在準備就緒後,進行等待(CountDownLatch.await()),直到秒殺時刻的到來,然後一擁而上;
這也是本地測試接口併發的一個簡易實現。

在這個場景中,CountDownLatch充當的是一個發令槍的角色;
就像田徑賽跑時,運動員會在起跑線做準備動作,等到發令槍一聲響,運動員就會奮力奔跑。和上面的秒殺場景類似,代碼實現如下:

CountDownLatch countDownLatch = new CountDownLatch(1);
for (int i = 0; i < 5; i++) {
    new Thread(() -> {
        try {
            //準備完畢……運動員都阻塞在這,等待號令
            countDownLatch.await();
            String parter = "【" + Thread.currentThread().getName() + "】";
            System.out.println(parter + "開始執行……");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
}

Thread.sleep(2000);// 裁判準備發令
countDownLatch.countDown();// 發令槍:執行發令

運行結果:

【Thread-0】開始執行……
【Thread-1】開始執行……
【Thread-4】開始執行……
【Thread-3】開始執行……
【Thread-2】開始執行……

我們通過CountDownLatch.await(),讓多個參與者線程啓動後阻塞等待,然後在主線程 調用CountDownLatch.countdown(1) 將計數減爲0,讓所有線程一起往下執行;
以此實現了多個線程在同一時刻併發執行,來模擬併發請求的目的。

場景2 讓單個線程等待:多個線程(任務)完成後,進行彙總合併

很多時候,我們的併發任務,存在前後依賴關係;比如數據詳情頁需要同時調用多個接口獲取數據,併發請求獲取到數據後、需要進行結果合併;或者多個數據操作完成後,需要數據check;
這其實都是:在多個線程(任務)完成後,進行彙總合併的場景。

代碼實現如下:

CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
    final int index = i;
    new Thread(() -> {
        try {
            Thread.sleep(1000 + ThreadLocalRandom.current().nextInt(1000));
            System.out.println("finish" + index + Thread.currentThread().getName());
            countDownLatch.countDown();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
}

countDownLatch.await();// 主線程在阻塞,當計數器==0,就喚醒主線程往下執行。
System.out.println("主線程:在所有任務運行完成後,進行結果彙總");

運行結果:

finish4Thread-4
finish1Thread-1
finish2Thread-2
finish3Thread-3
finish0Thread-0
主線程:在所有任務運行完成後,進行結果彙總

在每個線程(任務) 完成的最後一行加上CountDownLatch.countDown(),讓計數器-1;
當所有線程完成-1,計數器減到0後,主線程往下執行彙總任務。

示例源碼:https://github.com/ljheee/JavaConcurrencyInPractice/blob/master/src/main/java/com/ljheee/juc/CountDownLatchUsage.java

從上面兩個例子的代碼,可以看出 CountDownLatch 的API並不多;

  • CountDownLatch的構造函數中的count就是閉鎖需要等待的線程數量。這個值只能被設置一次,而且不能重新設置;

  • await():調用該方法的線程會被阻塞,直到構造方法傳入的 N 減到 0 的時候,才能繼續往下執行;

    調用了await()進行阻塞等待的線程,它們阻塞在Latch門閂/柵欄上;只有當條件滿足的時候(countDown() N次,將計數減爲0),它們才能同時通過這個柵欄;以此能夠實現,讓所有的線程站在一個起跑線上。

  • countDown():使 CountDownLatch 計數值 減 1;

CountDownLatch 工作原理

CountDownLatch是通過一個計數器來實現的,計數器的初始值爲線程的數量;
調用await()方法的線程會被阻塞,直到計數器 減到 0 的時候,才能繼續往下執行;
countDown()方法則是將計數器減1;

在CountDownLatch的構造函數中,指定的線程數量,只能指定一次;由於CountDownLatch採用的是減計數,因此當計數減爲0時,計數器不能被重置。
這是和CyclicBarrier的一個重要區別。

CountDownLatch 的源碼在JUC併發工具中,也相對算是簡單的;
底層基於 AbstractQueuedSynchronizer 實現,CountDownLatch 構造函數中指定的count直接賦給AQS的state;每次countDown()則都是release(1)減1,最後減到0時unpark阻塞線程;這一步是由最後一個執行countdown方法的線程執行的。
而調用await()方法時,當前線程就會判斷state屬性是否爲0,如果爲0,則繼續往下執行,如果不爲0,則使當前線程進入等待狀態,直到某個線程將state屬性置爲0,其就會喚醒在await()方法中等待的線程。

CountDownLatch與Thread.join

CountDownLatch的作用就是允許一個或多個線程等待其他線程完成操作,看起來有點類似join() 方法,但其提供了比 join() 更加靈活的API。
CountDownLatch可以手動控制在n個線程裏調用n次countDown()方法使計數器進行減一操作,也可以在一個線程裏調用n次執行減一操作。
而 join() 的實現原理是不停檢查join線程是否存活,如果 join 線程存活則讓當前線程永遠等待。所以兩者之間相對來說還是CountDownLatch使用起來較爲靈活。

CountDownLatch與CyclicBarrier

CountDownLatch和CyclicBarrier都能夠實現線程之間的等待,只不過它們側重點不同:

  • CountDownLatch一般用於一個或多個線程,等待其他線程執行完任務後,再才執行
  • CyclicBarrier一般用於一組線程互相等待至某個狀態,然後這一組線程再同時執行
    另外,CountDownLatch是減計數,計數減爲0後不能重用;而CyclicBarrier是加計數,可置0後複用。

更多CyclicBarrier的內容,請見下篇。

閱讀原文:https://mp.weixin.qq.com/s/RhK_BrYrooGGYbN5OvyXow


推薦閱讀

本文首發於 公衆號 架構道與術(ToBeArchitecturer),歡迎關注、學習更多幹貨~
在這裏插入圖片描述

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