本文內容如有錯誤、不足之處,歡迎技術愛好者們一同探討,在本文下面討論區留言,感謝。
簡介
CountDownLatch 在Java中是一種同步器,它允許一個線程在開始執行之前,等待一個或多個線程。
可以在程序中使用Java中的等待和通知機制來實現和CountDownLatch相同的功能 ,但是它需要大量代碼,並且在第一次使用時非常困難(tricky),而使用CountDownLatch 可以使用幾行代碼簡單完成。CountDownLatch 還允許靈活地等待主線程要等待的線程數,它可以等待一個線程或n個線程,代碼上沒有太大變化。關鍵是需要明白Java應用程序在哪裏使用CountDownLatch更好。
例如,應用程序的主線程要等待,直到負責啓動框架服務的其他服務線程完成了所有服務的啓動。
原理
CountDownLatch的工作原理是使用線程數初始化計數器,每次線程執行完成時,計數器都會遞減。當計數器個數(count)達到零時,表示所有線程已完成其執行,並且等待latch鎖的線程(例如:主線程)將恢復執行。
從圖片上可以看到,流程是TA線程調用其他三個線程,等待其他3個線程執行完成後,才執行TA線程剩下的邏輯。
過程如下
- 主線程啓動
- 創建包含N個線程的CountDownLatch
- 啓動N個線程
- 主線程等待N個線程執行完畢
- N個線程完成返回
- 主線程恢復執行
使用
CountDownLatch.java類的構造函數:
// 根據count創造初始化一個CountDownLatch
public CountDownLatch(int count) {...}
此計數count本質上是CountDownLatch 應等待的線程數。該值只能設置一次,並且CountDownLatch 沒有提供其他機制來重置此count。
使用CountDownLatch 時需要注意的兩點是:
- 一旦計數達到零,就不能重用CountDownLatch ,這是CountDownLatch和CyclicBarrier之間的主要區別。
- 主線程通過調用 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);
使用場景
- 實現最大並行測試,上面的例子中已經給出。
- 等待N個線程完成,然後再開始執行。
- 死鎖檢測,一個非常方便的用例,可以在每個測試階段使用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案例)