CountDownLatch
是 Java 中 java.util.concurrent
包提供的一個同步工具類,用于協調多個線程之間的執行順序。它允許一個或多個線程等待,直到其他線程完成一組操作后繼續執行。CountDownLatch
是一種倒計數鎖存器,通過設置一個初始計數器值,線程可以通過調用 countDown()
方法遞減計數器,當計數器達到 0 時,等待的線程會被喚醒。
主要特點
- 一次性使用:
CountDownLatch
是一次性工具,一旦計數器減到 0,它不能被重置或重用。如果需要可重置的機制,可以考慮使用CyclicBarrier
。 - 線程等待:一個或多個線程可以通過
await()
方法等待計數器歸零。 - 計數器遞減:通過
countDown()
方法,線程可以減少計數器的值。 - 線程安全:
CountDownLatch
是線程安全的,適合多線程并發場景。 - 靈活性:可以用于等待一組任務完成后再執行后續邏輯,或者讓主線程等待多個子線程完成。
工作原理
CountDownLatch
內部維護一個計數器,初始化時指定一個正整數值。- 調用
countDown()
方法會將計數器減 1,直到計數器為 0。 - 調用
await()
方法的線程會阻塞,直到計數器為 0,所有等待的線程會被喚醒。 - 使用基于 AQS(AbstractQueuedSynchronizer)的同步機制,確保線程安全和高效的等待/通知機制。
常見方法
CountDownLatch(int count)
:構造方法,初始化計數器為指定的值count
。void countDown()
:將計數器減 1,如果計數器達到 0,喚醒所有等待的線程。void await()
:使當前線程阻塞,直到計數器歸零。boolean await(long timeout, TimeUnit unit)
:使當前線程阻塞,直到計數器歸零或超時,返回是否成功(true 表示計數器歸零,false 表示超時)。long getCount()
:返回當前計數器的值。
使用場景
- 等待多個線程完成:主線程等待多個子線程完成任務后再繼續執行。
- 并行任務協調:在任務分解為多個子任務時,確保所有子任務完成后再進行匯總。
- 啟動信號:確保多個線程同時開始執行(通過設置計數器為 1,主線程調用
countDown()
觸發所有線程)。 - 測試并發場景:在性能測試中,模擬多個線程同時執行某個任務。
CountDownLatch 示例代碼
以下是幾個使用 CountDownLatch
的實際例子,展示其在不同場景下的應用。
示例 1:主線程等待多個子線程完成
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class CountDownLatchExample1 {public static void main(String[] args) throws InterruptedException {int threadCount = 3;// 創建一個計數器,初始值為3CountDownLatch latch = new CountDownLatch(threadCount);// 創建線程池ExecutorService executor = Executors.newFixedThreadPool(threadCount);// 提交三個任務for (int i = 1; i <= threadCount; i++) {final int taskId = i;executor.submit(() -> {try {System.out.println("任務 " + taskId + " 開始執行");Thread.sleep((int) (Math.random() * 1000)); // 模擬任務耗時System.out.println("任務 " + taskId + " 完成");latch.countDown(); // 計數器減1} catch (InterruptedException e) {e.printStackTrace();}});}// 主線程等待所有任務完成System.out.println("主線程等待所有任務完成...");latch.await();System.out.println("所有任務已完成,主線程繼續執行");// 關閉線程池executor.shutdown();}
}
輸出示例:
主線程等待所有任務完成...
任務 1 開始執行
任務 2 開始執行
任務 3 開始執行
任務 1 完成
任務 3 完成
任務 2 完成
所有任務已完成,主線程繼續執行
解釋:
- 創建一個
CountDownLatch
,初始計數器為 3。 - 三個線程分別執行任務,并在完成后調用
countDown()
減少計數器。 - 主線程調用
await()
,阻塞直到計數器為 0。 - 當所有線程完成任務后,計數器歸零,主線程繼續執行。
示例 2:模擬并發任務啟動
這個例子展示如何使用 CountDownLatch
讓多個線程同時開始執行任務。
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class CountDownLatchExample2 {public static void main(String[] args) throws InterruptedException {int threadCount = 5;// 創建一個計數器,初始值為1CountDownLatch startSignal = new CountDownLatch(1);// 創建線程池ExecutorService executor = Executors.newFixedThreadPool(threadCount);// 提交5個任務,等待啟動信號for (int i = 1; i <= threadCount; i++) {final int workerId = i;executor.submit(() -> {try {System.out.println("工人 " + workerId + " 準備就緒");startSignal.await(); // 等待啟動信號System.out.println("工人 " + workerId + " 開始工作");Thread.sleep((int) (Math.random() * 1000)); // 模擬工作System.out.println("工人 " + workerId + " 完成工作");} catch (InterruptedException e) {e.printStackTrace();}});}// 主線程模擬準備時間Thread.sleep(1000);System.out.println("所有工人準備就緒,主線程發出啟動信號!");startSignal.countDown(); // 發出啟動信號// 關閉線程池executor.shutdown();}
}
輸出示例:
工人 1 準備就緒
工人 2 準備就緒
工人 3 準備就緒
工人 4 準備就緒
工人 5 準備就緒
所有工人準備就緒,主線程發出啟動信號!
工人 1 開始工作
工人 2 開始工作
工人 3 開始工作
工人 4 開始工作
工人 5 開始工作
工人 3 完成工作
工人 1 完成工作
工人 5 完成工作
工人 2 完成工作
工人 4 完成工作
解釋:
- 使用
CountDownLatch
的計數器為 1,模擬一個啟動信號。 - 所有工人線程調用
await()
等待主線程的信號。 - 主線程調用
countDown()
后,所有工人線程幾乎同時開始工作。
示例 3:帶超時的等待
這個例子展示如何使用帶超時的 await
方法。
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;public class CountDownLatchExample3 {public static void main(String[] args) throws InterruptedException {int threadCount = 2;CountDownLatch latch = new CountDownLatch(threadCount);ExecutorService executor = Executors.newFixedThreadPool(threadCount);// 提交兩個任務executor.submit(() -> {try {System.out.println("任務 1 開始執行");Thread.sleep(1000); // 模擬耗時任務System.out.println("任務 1 完成");latch.countDown();} catch (InterruptedException e) {e.printStackTrace();}});executor.submit(() -> {try {System.out.println("任務 2 開始執行");Thread.sleep(3000); // 模擬更長的耗時任務System.out.println("任務 2 完成");latch.countDown();} catch (InterruptedException e) {e.printStackTrace();}});// 主線程等待,最多等待2秒System.out.println("主線程等待,最多2秒...");boolean completed = latch.await(2, TimeUnit.SECONDS);if (completed) {System.out.println("所有任務在2秒內完成");} else {System.out.println("超時,部分任務未完成");}executor.shutdown();}
}
輸出示例:
主線程等待,最多2秒...
任務 1 開始執行
任務 2 開始執行
任務 1 完成
超時,部分任務未完成
任務 2 完成
解釋:
- 主線程設置了 2 秒的超時時間,通過
await(2, TimeUnit.SECONDS)
。 - 任務 1 在 1 秒內完成,但任務 2 需要 3 秒,因此主線程在超時后繼續執行,輸出“部分任務未完成”。
CountDownLatch 與其他工具的對比
- 與 CyclicBarrier 的區別:
CountDownLatch
是一次性的,計數器歸零后無法重用;CyclicBarrier
可以重置并重復使用。CountDownLatch
通常用于主線程等待子線程;CyclicBarrier
更適合多個線程相互等待。
- 與 Semaphore 的區別:
Semaphore
用于控制資源訪問的并發數;CountDownLatch
用于協調線程的完成順序。
- 與 wait/notify:
CountDownLatch
提供了更簡單、更高級的同步機制,基于 AQS 實現,性能更優。
使用建議
- 適用場景:當需要等待一組任務完成后再執行后續邏輯,或需要多個線程同時開始時。
- 不適用場景:如果需要重復使用計數器,建議使用
CyclicBarrier
;如果需要控制并發訪問量,建議使用Semaphore
。 - 注意事項:
- 確保初始計數器值合理,過高的計數可能導致線程長時間等待。
countDown()
調用過多不會導致計數器變成負數,但應避免邏輯錯誤。- 使用帶超時的
await
方法可以避免無限等待。
總結
CountDownLatch
是一個簡單而強大的同步工具,適用于協調多線程任務的完成順序或統一啟動。通過設置計數器和使用 await
/countDown
方法,它能有效地控制線程間的協作。上述示例展示了其在等待任務完成、統一啟動并發任務和帶超時等待的場景中的應用。