CountDownLatch是什麼?

本文內容如有錯誤、不足之處,歡迎技術愛好者們一同探討,在本文下面討論區留言,感謝。

簡介

CountDownLatch 在Java中是一種同步器,它允許一個線程在開始執行之前,等待一個或多個線程。

可以在程序中使用Java中的等待和通知機制來實現和CountDownLatch相同的功能 ,但是它需要大量代碼,並且在第一次使用時非常困難(tricky),而使用CountDownLatch 可以使用幾行代碼簡單完成。CountDownLatch 還允許靈活地等待主線程要等待的線程數,它可以等待一個線程或n個線程,代碼上沒有太大變化。關鍵是需要明白Java應用程序在哪裏使用CountDownLatch更好。

例如,應用程序的主線程要等待,直到負責啓動框架服務的其他服務線程完成了所有服務的啓動。

原理

CountDownLatch的工作原理是使用線程數初始化計數器,每次線程執行完成時,計數器都會遞減。當計數器個數(count)達到零時,表示所有線程已完成其執行,並且等待latch鎖的線程(例如:主線程)將恢復執行。

在這裏插入圖片描述
從圖片上可以看到,流程是TA線程調用其他三個線程,等待其他3個線程執行完成後,才執行TA線程剩下的邏輯。

過程如下

  1. 主線程啓動
  2. 創建包含N個線程的CountDownLatch
  3. 啓動N個線程
  4. 主線程等待N個線程執行完畢
  5. N個線程完成返回
  6. 主線程恢復執行

使用

CountDownLatch.java類的構造函數:

// 根據count創造初始化一個CountDownLatch
public CountDownLatch(int count) {...}

此計數count本質上是CountDownLatch 應等待的線程數。該值只能設置一次,並且CountDownLatch 沒有提供其他機制來重置此count。

使用CountDownLatch 時需要注意的兩點是:

  1. 一旦計數達到零,就不能重用CountDownLatch ,這是CountDownLatch和CyclicBarrier之間的主要區別。
  2. 主線程通過調用 CountDownLatch.await()方法來等待latch線程完成,而其他線程則調用CountDownLatch.countDown()來通知它們已完成。

例子:

import java.util.concurrent.CountDownLatch; 
  
public class CountDownLatchDemo 
{ 
    public static void main(String args[])  
                   throws InterruptedException 
    { 
        // 創建一個線程等待其他4個線程執行完畢後再執行 
        CountDownLatch latch = new CountDownLatch(4); 
  
        // 創建並啓動4個線程
        Worker first = new Worker(1000, latch,  
                                  "WORKER-1"); 
        Worker second = new Worker(2000, latch,  
                                  "WORKER-2"); 
        Worker third = new Worker(3000, latch,  
                                  "WORKER-3"); 
        Worker fourth = new Worker(4000, latch,  
                                  "WORKER-4"); 
        first.start(); 
        second.start(); 
        third.start(); 
        fourth.start(); 
        // main-task 等待上面4個線程
        latch.await(); 
  
        // main-thread 開始工作
        System.out.println(Thread.currentThread().getName() + 
                           " 已經完成"); 
    } 
} 
  
// 資源類 
class Worker extends Thread 
{ 
    private int delay; 
    private CountDownLatch latch; 
    public Worker(int delay, CountDownLatch latch, 
                                    String name) 
    { 
        super(name); 
        this.delay = delay; 
        this.latch = latch; 
    } 
  
    @Override
    public void run() 
    { 
        try
        { 
            Thread.sleep(delay); 
            latch.countDown(); 
            System.out.println(Thread.currentThread().getName() 
                            + " 完成"); 
        } 
        catch (InterruptedException e) 
        { 
            e.printStackTrace(); 
        } 
      }
}

使用CountDownLatch 模擬併發錯誤出現的場景

實際項目中啓動了數千個線程,而不是四個線程,那麼許多較早執行的線程可能已經完成處理,甚至後面的線程還沒有調用start()方法的時候。這可能使嘗試重現併發問題變得困難,因爲無法使所有線程並行運行。

爲了解決這個問題,使CountdownLatch 的工作方式與前面的示例不同。除了在某些子線程完成之前阻塞父線程之外,需要在每個子線程都啓動之前阻塞每個子線程。等所有線程都到位並進行等待的時候,釋放這些等待的線程,這就好比1000個人參加100米跑步,所有人都站在起跑線上等待起跑槍聲,槍聲一響,所有運動員開始比賽,併發跑步。

修改run() 方法,使其在處理之前阻塞:

public class WaitingWorker implements Runnable {
 
 	// 輸出黑板
    private List<String> outputScraper;
    // 準備線程數latch
    private CountDownLatch readyThreadCounter;
    // 調用線程數latch
    private CountDownLatch callingThreadBlocker;
    // 計算完成線程數latch
    private CountDownLatch completedThreadCounter;
 
    public WaitingWorker(
      List<String> outputScraper,
      CountDownLatch readyThreadCounter,
      CountDownLatch callingThreadBlocker,
      CountDownLatch completedThreadCounter) {
 
        this.outputScraper = outputScraper;
        this.readyThreadCounter = readyThreadCounter;
        this.callingThreadBlocker = callingThreadBlocker;
        this.completedThreadCounter = completedThreadCounter;
    }
 @Override
    public void run() {
    	// 等待線程數減一,相當於運動員到達自己賽道
        readyThreadCounter.countDown();
        try {
        	// 等待調用,相當於運動員準備好等待起跑槍聲
            callingThreadBlocker.await();
            // 執行業務邏輯,相當於運動員跑步進行比賽
            doSomeWork();
            // 黑板輸入結果。
            outputScraper.add("Counted down");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
        	// 計算完成線程數,相當於到達終點的運動員
            completedThreadCounter.countDown();
        }
    }
}

下面的測試類,main方法阻塞直到所有Workers啓動,然後解除阻塞,然後再阻塞直到Workers完成:

@Test
public void whenDoingLotsOfThreadsInParallel_thenStartThemAtTheSameTime()
 throws InterruptedException {
  
  	// ArrayList是線程不安全類,需要調用Collections.synchronizedList()進行線程安全處理。
    List<String> outputScraper = Collections.synchronizedList(new ArrayList<>());
    CountDownLatch readyThreadCounter = new CountDownLatch(4);
    CountDownLatch callingThreadBlocker = new CountDownLatch(1);
    CountDownLatch completedThreadCounter = new CountDownLatch(4);
    // Stream流,生成4個線程數
    List<Thread> workers = Stream
      .generate(() -> new Thread(new WaitingWorker(
        outputScraper, readyThreadCounter, callingThreadBlocker, completedThreadCounter)))
      .limit(4)
      .collect(toList());
 
    workers.forEach(Thread::start);
    readyThreadCounter.await(); 
    outputScraper.add("Workers ready");
    // 啓動所有線程操作,將調用線程latch進行countDown解除所有線程等待,相當於跑步時的裁判發出的槍聲
    callingThreadBlocker.countDown(); 
    completedThreadCounter.await(); 
    outputScraper.add("Workers complete");
 
    assertThat(outputScraper)
      .containsExactly(
        "Workers ready",
        "Counted down",
        "Counted down",
        "Counted down",
        "Counted down",
        "Workers complete"
      );
}

這種方法可以用來迫使成千上萬的線程嘗試並行執行某些邏輯,因此這種模式對於嘗試重現併發錯誤非常有用。

超時調用處理

上面的代碼,可以看到對中斷異常進行了try-catch,因爲有些線程會發生中斷或者其他異常,如果某個線程的異常如下:

@Override
public void run() {
    if (true) {
        throw new RuntimeException("Oh dear, I'm a BrokenWorker");
    }
    countDownLatch.countDown();
    outputScraper.add("Counted down");
}

那麼,調用這個latch的線程將一直進行等待,無法繼續執行下去,因爲CountDownLatch 中的計數器Counter永遠不可能爲0,因此需要在await()的調用中添加一個超時參數。

boolean completed = countDownLatch.await(3L, TimeUnit.SECONDS);

使用場景

  1. 實現最大並行測試,上面的例子中已經給出。
  2. 等待N個線程完成,然後再開始執行。
  3. 死鎖檢測,一個非常方便的用例,可以在每個測試階段使用N個線程訪問具有不同數量線程的共享資源,試圖創建死鎖進行測試。

參考資料

How is CountDownLatch used in Java Multithreading? (在Java多線程中如何使用CountDownLatch?

Guide to CountDownLatch in Java (CountDownLatch指南

CountDownLatch in Java (Java中的CountDownLatch)

Java concurrency – CountDownLatch Example(CountDownLatch案例

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