一文徹底弄懂JUC工具包的CountDownLatch的設計理念與底層原理

lgx211發表於2024-11-09

CountDownLatch 是 Java 併發包(java.util.concurrent)中的一個同步輔助類,它允許一個或多個執行緒等待一組操作完成。

一、設計理念

CountDownLatch 是基於 AQS(AbstractQueuedSynchronizer)實現的。其核心思想是維護一個倒計數,每次倒計數減少到零時,等待的執行緒才會繼續執行。它的主要設計目標是允許多個執行緒協調完成一組任務。

1. 建構函式與計數器

public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}

構造 CountDownLatch 時傳入的 count 決定了計數器的初始值。該計數器控制了執行緒的釋放。

2. AQS 支援的核心操作

AQS 是 CountDownLatch 的基礎,透過自定義內部類 Sync 實現,Sync 繼承了 AQS 並提供了必要的方法。以下是關鍵操作:

  • acquireShared(int arg): 如果計數器值為零,表示所有任務已完成,執行緒將獲得許可。
  • releaseShared(int arg): 每次呼叫 countDown(),會減少計數器,當計數器降到零時,AQS 將釋放所有等待的執行緒。

3. 實現細節

  • countDown():呼叫 releaseShared() 減少計數器,並通知等待執行緒。
  • await():呼叫 acquireSharedInterruptibly(1),如果計數器非零則阻塞等待。

二、底層原理

CountDownLatch 的核心是基於 AbstractQueuedSynchronizer(AQS)來管理計數器狀態的。AQS 是 JUC 中許多同步工具的基礎,透過一個獨佔/共享模式的同步佇列實現執行緒的管理和排程。CountDownLatch 採用 AQS 的共享鎖機制來控制多個執行緒等待一個條件。

1. AQS 的共享模式

AQS 設計了兩種同步模式:獨佔模式(exclusive)和共享模式(shared)。CountDownLatch 使用共享模式:

  • 獨佔模式:每次只能一個執行緒持有鎖,如 ReentrantLock
  • 共享模式:允許多個執行緒共享鎖狀態,如 SemaphoreCountDownLatch

CountDownLatchawait()countDown() 方法對應於 AQS 的 acquireShared()releaseShared() 操作。acquireShared() 會檢查同步狀態(計數器值),若狀態為零則立即返回,否則阻塞當前執行緒,進入等待佇列。releaseShared() 用於減少計數器並喚醒所有等待執行緒。

2. Sync 內部類的設計

CountDownLatch 透過一個私有的內部類 Sync 來實現同步邏輯。Sync 繼承自 AQS,並重寫 tryAcquireShared(int arg)tryReleaseShared(int arg) 方法。

static final class Sync extends AbstractQueuedSynchronizer {
    Sync(int count) {
        setState(count);
    }

    protected int tryAcquireShared(int acquires) {
        return (getState() == 0) ? 1 : -1;
    }

    protected boolean tryReleaseShared(int releases) {
        // 自旋減計數器
        for (;;) {
            int c = getState();
            if (c == 0)
                return false;
            int nextc = c - 1;
            if (compareAndSetState(c, nextc))
                return nextc == 0;
        }
    }
}
  • tryAcquireShared(int):當計數器為零時返回 1(成功獲取鎖),否則返回 -1(阻塞)。
  • tryReleaseShared(int):每次 countDown() 減少計數器值,當計數器到達零時返回 true,喚醒所有阻塞執行緒。

3. CAS 操作確保執行緒安全

tryReleaseShared 方法使用 CAS(compare-and-set)更新計數器,避免了鎖的開銷。CAS 操作由 CPU 原語(如 cmpxchg 指令)支援,實現了高效的非阻塞操作。這種設計保證了 countDown() 的執行緒安全性,使得多個執行緒能夠併發地減少計數器。

4. 內部的 ConditionObject

CountDownLatch 不支援複用,因為 AQS 的 ConditionObject 被設計為單一觸發模式。計數器一旦降至零,CountDownLatch 無法重置,只能釋放所有執行緒,而不能再次設定初始計數器值。這就是其不可複用的根本原因。

三、應用場景

  1. 等待多執行緒任務完成CountDownLatch 常用於需要等待一組執行緒完成其任務後再繼續的場景,如批處理任務。
  2. 並行執行再彙總:在某些資料分析或計算密集型任務中,將任務分割成多個子任務並行執行,主執行緒等待所有子任務完成後再彙總結果。
  3. 多服務依賴協調:當一個服務依賴多個其他服務時,可以使用 CountDownLatch 來同步各個服務的呼叫,並確保所有依賴服務準備好之後再執行主任務。

四、示例程式碼

以下示例展示如何使用 CountDownLatch 實現一個併發任務等待所有子任務完成的機制。

import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    private static final int TASK_COUNT = 5;
    private static CountDownLatch latch = new CountDownLatch(TASK_COUNT);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < TASK_COUNT; i++) {
            new Thread(new Task(i + 1, latch)).start();
        }
        
        // 主執行緒等待所有任務完成
        latch.await();
        System.out.println("所有任務已完成,繼續主執行緒任務");
    }

    static class Task implements Runnable {
        private final int taskNumber;
        private final CountDownLatch latch;

        Task(int taskNumber, CountDownLatch latch) {
            this.taskNumber = taskNumber;
            this.latch = latch;
        }

        @Override
        public void run() {
            try {
                System.out.println("子任務 " + taskNumber + " 開始執行");
                Thread.sleep((int) (Math.random() * 1000)); // 模擬任務執行時間
                System.out.println("子任務 " + taskNumber + " 完成");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                latch.countDown(); // 完成一個任務,計數器減一
            }
        }
    }
}

五、與其他同步工具的對比

1. CyclicBarrier

原理和用途

  • CyclicBarrier 也允許一組執行緒相互等待,直到所有執行緒到達屏障位置(barrier point)。
  • 它適合用於多階段任務分階段匯聚,如處理分塊計算時每階段彙總結果。

底層實現

  • CyclicBarrier 內部透過 ReentrantLockCondition 實現,屏障次數可以重置,從而支援迴圈使用。

與 CountDownLatch 的對比

  • CyclicBarrier可複用性使其適合重複的同步場景,而 CountDownLatch 是一次性的。
  • CountDownLatch 更靈活,允許任意執行緒呼叫 countDown(),適合分散式任務。CyclicBarrier 需要指定的執行緒達到屏障。

2. Semaphore

原理和用途

  • Semaphore 主要用於控制資源訪問的併發數量,如限制資料庫連線池的訪問。

底層實現

  • Semaphore 基於 AQS 的共享模式實現,類似於 CountDownLatch,但允許透過指定的“許可證”數量控制資源。

與 CountDownLatch 的對比

  • Semaphore 可以動態增加/減少許可,而 CountDownLatch 只能遞減。
  • Semaphore 適合控制訪問限制,而 CountDownLatch 用於同步點倒計數。

3. Phaser

原理和用途

  • PhaserCyclicBarrier 的增強版,允許動態調整參與執行緒的數量。
  • 適合多階段任務同步,並能隨時增加或減少參與執行緒。

底層實現

  • Phaser 內部包含一個計數器,用於管理當前階段的參與執行緒,允許任務動態註冊或登出。

與 CountDownLatch 的對比

  • Phaser 更適合複雜場景,能夠靈活控制階段和參與執行緒;CountDownLatch 的結構簡單,只能用於一次性同步。
  • Phaser 的設計更復雜,適合長時間、多執行緒協調任務,而 CountDownLatch 更適合簡單任務等待。

4、總結

CountDownLatch 是一個輕量級、不可複用的倒計數同步器,適合簡單的一次性執行緒協調。其基於 AQS 的共享鎖實現使得執行緒等待和計數器更新具有高效的併發性。雖然 CountDownLatch 不具備重用性,但其設計簡潔,尤其適合需要等待多執行緒任務完成的場景。

與其他 JUC 工具相比:

  • CyclicBarrier 更適合多階段同步、階段性彙總任務。
  • Semaphore 適合資源訪問控制,具有可控的許可量。
  • Phaser 靈活性更高,適合動態參與執行緒、複雜多階段任務。

選擇適合的同步工具,取決於任務的性質、執行緒參與動態性以及是否需要重用同步控制。

相關文章