鉑金07:整齊劃一-CountDownLatch如何協調多執行緒的開始和結束

秦二爺發表於2021-07-02

21 2

歡迎來到《併發王者課》,本文是該系列文章中的第20篇

在上一篇文章中,我們介紹了Condition的用法。在本文中,將為你介紹CountDownLatch的用法。CountDownLatch是JUC中的一款常用工具類,當你在編寫多執行緒程式碼時,如果你需要協調多個執行緒的開始和結束動作時,它可能會是你的不錯選擇

一、CountDownLatch適用的兩個典型應用場景

場景1. 協調子執行緒結束動作:等待所有子執行緒執行結束

對於資深的王者來說,下面這幅圖一定再熟悉不過了。在王者開局匹配隊友時,所有的玩家都必須進行確認操作,只有全部確認後才可以進入遊戲,否則將進行重新匹配。如果我們把各玩家看作是子執行緒的話,那麼就需要各子執行緒完成確認動作,遊戲才能繼續。其實,此類場景不止於王者,在生活中類似的場景還有很多。比如,所有乘客登機後飛機才能關艙門,除非超時後他們拋棄了你。

對於上圖所示的玩家確認介面,如果用多執行緒模擬的話,那麼它應該是下面的樣子:

主執行緒建立了5個子執行緒,各子任務執行確認動作,期間主執行緒進入等待狀態,直到各子執行緒的任務均已經完成,主執行緒恢復繼續執行,也就是遊戲繼續。而如果其中某個玩家超時未執行確認的話,那麼主執行緒將結束本次匹配,重新開始新一輪的匹配。

這個場景,就是CountDownLatch適用的第一個經典場景。

場景2. 協調子執行緒開始動作:統一各執行緒動作開始的時機

這個場景的例子也十分常見。比如,田徑場上,各選手各就各位等待發令槍。在發令槍響之前,選手只能原地就位,否則就是違規。如果從多執行緒的角度看,這恰似你建立了一些多執行緒,但是你需要統一管理它們的任務開始時間。因為,如果你不對此做干預的話,執行緒呼叫start()之後的具體時間是不確定的,這個知識點我們早在青銅系列文章中就已經講過

在王者中也有類似的場景,遊戲開始時,各玩家的初始狀態必須一致。總不能,你剛降生,對方已經打到你家門口了

上述的兩個場景的問題,正是CountDownLatch所要解決的問題。理解了這兩個問題,你也就理解了CountDownLatch存在的價值。

二、Java中的CountDownLatch設計

JUC中CountDownLatch的實現,是以類的形式存在,而不是介面,你可以直接拿過來使用。並且,它的實現還很簡單。在資料結構上,CountDownLatch基於一個同步器實現,你可以看它的final Sync sync變數。

而在建構函式上,CountDownLatch有且只有CountDownLatch(int count)一個構造器,並且你需要指定數量,並且你不得在中途修改它,這點務必牢記

核心函式

  • await():等待latch降為0;
  • boolean await(long timeout, TimeUnit unit):等待latch降為0,但是可以設定超時時間。比如有玩家超時未確認,那就重新匹配,總不能為了某個玩家等到天荒地老吧。
  • countDown():latch數量減1;
  • getCount():獲取當前的latch數量。

從CountDownLatch的方法上看,還是比較簡單易懂的,重點要理解await()countDown().

三、CountDownLatch如何解決場景問題

接下來,我們將第一小節的兩個場景問題用程式碼實現一遍,讓你對CountDownLatch的用法有個直觀的理解。

場景1. CountDownLatch實現對各子執行緒的等待

建立大喬、蘭陵王、安其拉、哪吒和鎧等五個玩家,主執行緒必須在他們都完成確認後,才可以繼續執行。

在這段程式碼中,new CountDownLatch(5)使用者建立初始的latch數量,各玩家通過countDownLatch.countDown()完成狀態確認。

public static void main(String[] args) throws InterruptedException {
    CountDownLatch countDownLatch = new CountDownLatch(5);

    Thread 大喬 = new Thread(countDownLatch::countDown);
    Thread 蘭陵王 = new Thread(countDownLatch::countDown);
    Thread 安其拉 = new Thread(countDownLatch::countDown);
    Thread 哪吒 = new Thread(countDownLatch::countDown);
    Thread 鎧 = new Thread(() -> {
        try {
            // 稍等,上個衛生間,馬上到...
            Thread.sleep(1500);
            countDownLatch.countDown();
        } catch (InterruptedException ignored) {}
    });

    大喬.start();
    蘭陵王.start();
    安其拉.start();
    哪吒.start();
    鎧.start();
    countDownLatch.await();
    System.out.println("所有玩家已經就位!");
}

場景2. CountDownLatch實現對多執行緒的統一管理

在這個場景中,我們仍然用五個執行緒代表大喬、蘭陵王、安其拉、哪吒和鎧等五個玩家。需要注意的是,各玩家雖然都呼叫了start()執行緒,但是它們在執行時都在等待countDownLatch的訊號,在訊號未收到前,它們不會往下執行

public static void main(String[] args) throws InterruptedException {
    CountDownLatch countDownLatch = new CountDownLatch(1);

    Thread 大喬 = new Thread(() -> waitToFight(countDownLatch));
    Thread 蘭陵王 = new Thread(() -> waitToFight(countDownLatch));
    Thread 安其拉 = new Thread(() -> waitToFight(countDownLatch));
    Thread 哪吒 = new Thread(() -> waitToFight(countDownLatch));
    Thread 鎧 = new Thread(() -> waitToFight(countDownLatch));

    大喬.start();
    蘭陵王.start();
    安其拉.start();
    哪吒.start();
    鎧.start();
    Thread.sleep(1000);
    countDownLatch.countDown();
    System.out.println("敵方還有5秒達到戰場,全軍出擊!");
}

private static void waitToFight(CountDownLatch countDownLatch) {
    try {
        countDownLatch.await(); // 在此等待訊號再繼續
        System.out.println("收到,發起進攻!");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

執行結果如下,各玩家在收到出擊訊號發起了進攻:

敵方還有5秒達到戰場,全軍出擊!
收到,發起進攻!
收到,發起進攻!
收到,發起進攻!
收到,發起進攻!
收到,發起進攻!

Process finished with exit code 0

小結

以上就是關於CountDownLatch的全部內容。總體上,CountDownLatch比較簡單且易於理解。在學習時,先了解其設計意圖,再寫個Demo基本就能流暢掌握。

正文到此結束,恭喜你又上了一顆星✨

夫子的試煉

  • 編寫程式碼體驗CountDownLatch用法。

延伸閱讀與參考資料

最新修訂及更好閱讀體驗

關於作者

關注【技術八點半】,及時獲取文章更新。傳遞有品質的技術文章,記錄平凡人的成長故事,偶爾也聊聊生活和理想。早晨8:30推送作者品質原創,晚上20:30推送行業深度好文。

如果本文對你有幫助,歡迎點贊關注監督,我們一起從青銅到王者

相關文章