承接上篇 await() 分析,本文深入剖析 countDown() 如何透過 AQS 共享模式遞減狀態並喚醒等待執行緒。
public void countDown() {
sync.releaseShared(1); // 呼叫 AQS 共享釋放
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { // 嘗試釋放共享資源
doReleaseShared(); // 喚醒後繼節點
return true;
}
return false;
}
protected boolean tryReleaseShared(int releases) {
for (;;) { // 自旋保證 CAS 成功
int c = getState(); // 獲取當前計數
if (c == 0) return false; // 已歸零,無需釋放
int nextc = c - 1; // 遞減
if (compareAndSetState(c, nextc)) { // CAS 更新狀態
return nextc == 0; // 歸零返回 true,觸發 doReleaseShared
}
// CAS 失敗則繼續自旋
}
}
關鍵點:state 從 N 遞減至 0,只有當遞減後為 0 時才返回 true,通知 AQS 執行喚醒流程。
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
// 頭節點處於等待喚醒狀態
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue;
unparkSuccessor(h); // 喚醒後繼節點
} else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) {
// 傳播共享狀態
continue;
}
}
if (h == head) break; // 頭節點未變更,結束
}
}
此方法將同步佇列頭節點狀態設為 PROPAGATE,確保共享釋放訊號向後傳播,喚醒所有因 await() 掛起的執行緒。
等待執行緒在 doAcquireSharedInterruptibly 中自旋檢查:
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg); // state == 0 返回 1
if (r >= 0) {
setHeadAndPropagate(node, r); // 設為新頭節點並傳播
return;
}
}
setHeadAndPropagate 同樣呼叫 doReleaseShared(),形成雙重保障機制。
public class TestCountDownLatch {
private static final int TASK_NUM = 5;
static class MyRunnable implements Runnable {
private final int num;
private final CountDownLatch latch;
MyRunnable(int num, CountDownLatch latch) { this.num = num; this.latch = latch; }
@Override
public void run() {
System.out.println("第 " + num + " 個執行緒開始執行任務...");
try { Thread.sleep(5000); } catch (InterruptedException ignored) {}
System.out.println("第 " + num + " 個執行緒任務結束...");
latch.countDown();
}
}
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(TASK_NUM);
for (int i = 1; i <= TASK_NUM; i++) {
new Thread(new MyRunnable(i, latch)).start();
}
System.out.println("主執行緒等待其他執行緒完成...");
latch.await();
System.out.println("所有任務完成,主執行緒繼續執行...");
}
}
| 優點 | 缺點 |
|---|---|
| API 簡單,單一計數器即可協調多執行緒 | 計數器不可重置,無法重複使用 |
| 基於 AQS 共享鎖,效能優異 | 不支援動態新增任務 |
| 釋放與獲取語義清晰,適合「一等多」場景 | 若需循環等待,請改用 CyclicBarrier 或 Phaser |