一站式了解Java中Semaphore的基本用法
引言
我們今天一起來了解一下JUC的同步工具類-Semaphore的基本用法。
什么是Semaphore(信號量)
Semaphore (信號量) 是 java.util.concurrent 包下非常有用的一個并發(fā)工具類。你可以把它理解為用于控制同時訪問特定資源的線程數(shù)量的工具。它維護(hù)了一組“許可”(permits),線程在訪問受保護(hù)資源前必須先獲取一個許可,使用完后再釋放該許可。
場景引入
想象一個只有 5 個車位的停車場(共享資源):
- Permits (許可證/令牌): 初始化時,Semaphore 擁有 5 個令牌。
- Acquire (獲取): 車輛(線程)進(jìn)入時,先領(lǐng) 1 個令牌。如果沒有令牌了(計數(shù)為0),車輛就得在門口排隊(阻塞)。
- Release (釋放): 車輛離開時,歸還 1 個令牌。此時如果有排隊的車輛,就會喚醒其中一個進(jìn)入。
這就是Semaphore做的事,控制在一個時間段內(nèi)可以訪問特定資源的線程數(shù)量。
基本概念
- 許可(Permit) :Semaphore 內(nèi)部維護(hù)一個計數(shù)器,表示當(dāng)前可用的許可數(shù)量。
- acquire() :嘗試獲取一個或多個許可。如果沒有足夠許可,線程會被阻塞,直到有足夠許可可用。
- release() :釋放一個或多個許可,增加可用許可數(shù)量,可能喚醒等待中的線程。
底層原理
Semaphore 內(nèi)部基于 AQS (AbstractQueuedSynchronizer) 實(shí)現(xiàn)。
- 它使用 AQS 的
state變量來存儲當(dāng)前的許可證數(shù)量。 acquire()操作是對state進(jìn)行 CAS 減法。release()操作是對state進(jìn)行 CAS 加法。
常用方法
下面是Semaphore的常用方法。
| 方法 | 描述 | 場景 |
|---|---|---|
| new Semaphore(int permits) | 創(chuàng)建非公平信號量(默認(rèn))。 | 吞吐量優(yōu)先 |
| new Semaphore(int permits, boolean fair) | 創(chuàng)建公平信號量(FIFO)。 | 避免線程饑餓 |
| acquire() | 獲取1個許可,若無則阻塞。響應(yīng)中斷。 | 必須拿到的場景 |
| acquire(int n) | 一次獲取 n 個許可。 | 批量資源 |
| tryAcquire() | 嘗試獲取,成功返回 true,失敗返回 false,不阻塞。 | 快速失敗/降級處理 |
| tryAcquire(long timeout, TimeUnit unit) | 帶超時的嘗試獲取。 | 避免長時間死等 |
| release() | 釋放1個許可。 | 務(wù)必在 finally 中調(diào)用 |
| void release(int permits) | 釋放指定數(shù)量許可。 | 務(wù)必在 finally 中調(diào)用 |
使用例子
這是最典型的使用場景:限流 (Rate Limiting) 或 資源池控制。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class SemaphoreExample {
// 定義一個只能允許3個線程同時訪問的信號量
private static final Semaphore semaphore = new Semaphore(3);
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10);
// 模擬10個請求同時涌入
for (int i = 0; i < 10; i++) {
final int threadNum = i;
executor.execute(() -> {
try {
// 1. 獲取許可 (如果拿不到,這里會阻塞)
// 也可以使用 tryAcquire() 來進(jìn)行非阻塞嘗試
semaphore.acquire();
System.out.println("線程 " + threadNum + " 拿到了令牌,正在處理業(yè)務(wù)...");
// 模擬業(yè)務(wù)耗時
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 2. 關(guān)鍵:一定要在 finally 中釋放許可!
System.out.println("線程 " + threadNum + " 處理完畢,歸還令牌 ---");
semaphore.release();
}
});
}
executor.shutdown();
}
}
一般使用場景有這些,特別是框架特別喜歡用:
- 數(shù)據(jù)庫連接池(限制最大連接數(shù))
- 線程池任務(wù)提交限流
- 文件讀寫并發(fā)控制
- 服務(wù)接口的 QPS 限流
平時使用的坑你要注意
場景 A:公平性 (Fairness)
new Semaphore(3, true) 會保證等待最久的線程最先拿到令牌。
- 優(yōu)點(diǎn): 避免線程饑餓。
- 缺點(diǎn): 性能較差(因?yàn)橐S護(hù)嚴(yán)格的隊列順序,增加了上下文切換開銷)。通常后端高并發(fā)場景默認(rèn)使用非公平模式以換取吞吐量。
場景 B:初始化為 0
Semaphore 不一定要初始化為正數(shù)。如果你初始化為 new Semaphore(0):
- 線程 A 調(diào)用
acquire()會立刻阻塞。 - 直到線程 B 調(diào)用
release(),線程 A 才會繼續(xù)執(zhí)行。 - 用途: 這種模式類似
CountDownLatch或Exchanger,用于線程間的單次信號通知。
常見坑
- 忘記 Release: 如果在異常發(fā)生前沒有釋放,或者沒寫在 finally 塊中,在這個 JVM 進(jìn)程重啟前,那個令牌就永久丟失了(類似內(nèi)存泄漏),最終導(dǎo)致所有線程阻塞。所以千萬要記住,寫release在finally塊!!
- Release 濫用: release() 只是簡單地將計數(shù)器 +1。如果你在沒有 acquire 的情況下錯誤地調(diào)用了多次 release(),信號量的許可證數(shù)量會超過初始值(比如變成 5+1 = 6),導(dǎo)致限流失效。
和平時的ReentrantLock / synchronized 的區(qū)別
使用用途和目的是它們最大的區(qū)別。
| 特性 | synchronized / ReentrantLock | Semaphore |
|---|---|---|
| 控制粒度 | 一次只允許一個線程進(jìn)入臨界區(qū) | 可允許多個線程(≤許可數(shù))同時進(jìn)入 |
| 是否可重入 | 是(ReentrantLock) | 否(許可不屬于特定線程) |
| 主要用途 | 互斥訪問 | 資源池、限流、并發(fā)控制 |
那么使用自定義計時器+鎖+喚醒等待機(jī)制也是可以實(shí)現(xiàn)Semaphore一樣的功能,但是沒必要,還容易出錯。不如直接使用Semaphore。
總結(jié)
Semaphore就是用來限制同時訪問統(tǒng)一資源的工具。
除了平時開發(fā)使用到Semaphore,我們平時面試做有關(guān)于多線程的算法題也有可能用到噢,所以還是非常有必要熟悉Semaphore的。
到此這篇關(guān)于一站式了解Java中Semaphore的基本用法的文章就介紹到這了,更多相關(guān)Java Semaphore用法內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring?Boot數(shù)據(jù)響應(yīng)問題實(shí)例詳解
這篇文章主要給大家介紹了關(guān)于Spring?Boot數(shù)據(jù)響應(yīng)問題的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2022-03-03
SpringCloud之Zuul網(wǎng)關(guān)原理及其配置講解
這篇文章主要介紹了SpringCloud之Zuul網(wǎng)關(guān)原理及其配置講解,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-03-03

