一文徹底弄懂JUC工具包的Semaphore

lgx211發表於2024-11-10

Semaphore 是 Java 併發包 (java.util.concurrent) 中的重要工具,主要用於控制多執行緒對共享資源的併發訪問量。它可以設定“許可證”(permit)的數量,並允許指定數量的執行緒同時訪問某一資源,適合限流、資源池等場景。下面從原始碼設計、底層原理、應用場景、以及與其它 JUC 工具的對比來詳細剖析 Semaphore。

一、Semaphore 的基本原理

Semaphore 本質上是一種計數訊號量,內部維護一個許可計數,每個執行緒在進入時需要申請一個許可(acquire),完成後釋放該許可(release)。當許可計數為零時,其他執行緒會阻塞,直到有執行緒釋放許可。

1. 計數和許可

  • 許可數:Semaphore 在初始化時設定許可數,通常表示可以同時訪問資源的執行緒數量。
  • 計數增減:呼叫 acquire() 時減少許可數,呼叫 release() 時增加許可數。
  • 公平性:Semaphore 可以設定為“公平”模式,保證執行緒按 FIFO 順序獲得許可。預設是“非公平”模式,效能更高,但執行緒獲取許可的順序不保證。

二、底層實現與原始碼解析

Semaphore 基於 AbstractQueuedSynchronizer (AQS) 實現,其實現方式和 CountDownLatch 類似,但使用了 AQS 的共享模式,並對許可計數進行精確管理。

1. AQS 的共享模式

Semaphore 使用 AQS 的共享模式(Shared),其中 AQS 的 state 表示剩餘許可數。acquireShared() 方法用於申請許可,releaseShared() 方法用於釋放許可。

2. 內部類 Sync 的實現

Semaphore 的核心實現依賴於其內部類 Sync,Sync 是 AbstractQueuedSynchronizer 的子類。根據是否是公平模式,有兩種實現:NonfairSyncFairSync

  • NonfairSync:非公平模式。執行緒呼叫 acquire 時直接嘗試獲取許可,不保證順序。
  • FairSync:公平模式。執行緒獲取許可遵循佇列順序。
abstract static class Sync extends AbstractQueuedSynchronizer {
    Sync(int permits) {
        setState(permits);  // 設定初始許可數
    }

    final int getPermits() {
        return getState();
    }

    final int nonfairTryAcquireShared(int acquires) {
        for (;;) {
            int available = getState();
            int remaining = available - acquires;
            if (remaining < 0 || compareAndSetState(available, remaining))
                return remaining;
        }
    }

    protected final boolean tryReleaseShared(int releases) {
        for (;;) {
            int current = getState();
            int next = current + releases;
            if (compareAndSetState(current, next))
                return true;
        }
    }
}
  • 非公平模式(nonfairTryAcquireShared:直接嘗試獲取許可,如果成功則更新 state
  • 公平模式(FairSync:遵循 AQS 的佇列順序,確保 FIFO 訪問。

三、Semaphore 的使用方法

Semaphore 提供了三種主要方法來操作許可:

  • acquire():獲取一個許可,若沒有許可則阻塞。
  • release():釋放一個許可,喚醒等待的執行緒。
  • tryAcquire():嘗試獲取許可,但不阻塞。
Semaphore semaphore = new Semaphore(3); // 初始許可數為 3

// 獲取許可,若無可用許可則阻塞
semaphore.acquire();      

// 釋放許可
semaphore.release();

// 嘗試獲取許可,若不可用立即返回 false,不會阻塞
if (semaphore.tryAcquire()) {
    // do something
}

acquire() 原始碼

public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

acquireSharedInterruptibly(1) 會呼叫 nonfairTryAcquireShared(1)tryAcquireShared(公平模式),嘗試獲取許可,若沒有足夠許可則阻塞等待。

release() 原始碼

public void release() {
    sync.releaseShared(1);
}

releaseShared(1) 增加許可數並喚醒阻塞執行緒,使等待執行緒得以繼續執行。

四、Semaphore 的應用場景

Semaphore 非常適合控制對有限資源的訪問,典型的應用場景有:

1. 連線池限流

在資料庫連線池中,可以使用 Semaphore 限制同時訪問連線的執行緒數。例如,資料庫連線數有限,透過 Semaphore 控制同時訪問的執行緒數量,避免過度負載。

public class DatabaseConnectionPool {
    private static final int MAX_CONNECTIONS = 5;
    private final Semaphore semaphore = new Semaphore(MAX_CONNECTIONS);

    public Connection getConnection() throws InterruptedException {
        semaphore.acquire();
        return acquireConnection();
    }

    public void releaseConnection(Connection conn) {
        releaseConnection(conn);
        semaphore.release();
    }
}

2. 控制執行緒併發數

在高併發任務中,Semaphore 可限制同時執行的執行緒數量,例如,控制下載任務的併發數,避免過多執行緒導致的系統負載過高。

3. 資源分配和限流

Semaphore 適合資源共享場景,如共享印表機、固定執行緒數的執行緒池等,確保資源不會被過度佔用。

五、與其他 JUC 工具的對比

1. CountDownLatch

  • 許可機制:Semaphore 的許可數可以動態變化,而 CountDownLatch 的計數器只能遞減。
  • 適用場景:Semaphore 控制併發資源訪問數量,而 CountDownLatch 用於執行緒等待。

2. CyclicBarrier

  • 作用:Semaphore 適合控制資源訪問併發數,而 CyclicBarrier 則用於執行緒間的同步點,使所有執行緒達到某個屏障時一起繼續。
  • 靈活性:Semaphore 更靈活,可隨時 release(),不必等待所有執行緒。

3. ReentrantLock

  • 模式:ReentrantLock 是排他鎖,每次只允許一個執行緒訪問。Semaphore 允許多個執行緒共享資源(多個許可)。
  • 適用場景:ReentrantLock 適合獨佔資源場景,Semaphore 適合資源池、限流等。

4. Phaser

  • 複雜度:Phaser 用於複雜的多階段同步,可以動態增加/減少執行緒,而 Semaphore 主要用於固定數量資源控制。
  • 適用性:Phaser 適合分階段任務同步,而 Semaphore 適合控制資源併發數。

廬山煙雨浙江潮,未至千般恨不消。

到得還來別無事,廬山煙雨浙江潮。

近半年不會再更新了,居家辦公的日子要結束了。讀書,世界就在眼前;不讀書,眼前就是世界。與你共勉!

相關文章