Semaphore 信号量机制:限流与并发控制的经典实现
在高并发场景下,我们常常需要控制同一时间允许访问某个资源的线程数量同时最多 10 个线程访问数据库连接池;同时最多 3 个线程下载文件;请求接口做限流控制。这些场景都可以用Semaphore(信号量)来实现。Semaphore 通过“许可证(permit)”机制控制并发数量,线程在执行前必须先获取许可证,执行后释放许可证。它是 JUC 中基于实现的同步工具,用途类似“计数器型锁”,但比锁更灵活。参
文章目录
《Semaphore 信号量机制:限流与并发控制的经典实现》
一、前言:为什么要用 Semaphore
在高并发场景下,我们常常需要控制同一时间允许访问某个资源的线程数量,
比如:
- 同时最多 10 个线程访问数据库连接池;
- 同时最多 3 个线程下载文件;
- 请求接口做限流控制。
这些场景都可以用 Semaphore(信号量) 来实现。
Semaphore 通过“许可证(permit)”机制控制并发数量,
线程在执行前必须先获取许可证,执行后释放许可证。
它是 JUC 中基于 AQS(AbstractQueuedSynchronizer) 实现的同步工具,
用途类似“计数器型锁”,但比锁更灵活。
二、核心概念与方法介绍
(1)构造方法
Semaphore semaphore = new Semaphore(int permits);
Semaphore semaphore = new Semaphore(int permits, boolean fair);
| 参数 | 含义 |
|---|---|
| permits | 可用许可证数量 |
| fair | 是否使用公平模式(FIFO) |
(2)核心方法
| 方法 | 说明 |
|---|---|
acquire() |
获取一个许可,若无则阻塞 |
acquire(int n) |
获取多个许可 |
tryAcquire() |
尝试获取,不阻塞 |
tryAcquire(long timeout, TimeUnit unit) |
超时获取 |
release() |
释放一个许可 |
release(int n) |
释放多个许可 |
availablePermits() |
返回当前可用许可数 |
drainPermits() |
一次性清空所有许可 |
简而言之:
acquire() → 占用资源;release() → 释放资源。
三、底层实现原理(AQS 支撑)
(1)内部类结构
Semaphore 内部通过继承 AQS 实现两种模式:
static final class NonfairSync extends Sync {}
static final class FairSync extends Sync {}
两者的差异仅在于获取许可时是否遵循 FIFO 排队。
(2)核心字段
private final Sync sync; // 继承自 AQS
AQS 的 state 变量记录当前可用许可证数量:
state = permits
(3)获取许可(acquire)逻辑
简化源码:
protected int tryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
逻辑说明:
- 获取当前剩余许可数;
- 若不足,则阻塞当前线程;
- 若足够,则用 CAS 减少许可数并立即返回。
(4)释放许可(release)逻辑
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
if (compareAndSetState(current, next))
return true;
}
}
释放操作会唤醒等待队列中的线程继续获取许可。
四、公平与非公平模式
| 模式 | 说明 | 特点 |
|---|---|---|
| 非公平模式(默认) | 线程到达后立即尝试获取许可,不管是否排队 | 吞吐量高,但可能“插队” |
| 公平模式 | 严格按照等待顺序(FIFO) | 公平但性能略低 |
源码对比:
// 非公平
if (compareAndSetState(available, remaining)) return remaining;
// 公平
if (hasQueuedPredecessors()) return -1;
在限流系统中,公平模式可以避免“饿死”线程;
而在性能优先场景中,非公平模式更常用。
五、典型应用场景与实战示例
(1)限流控制
限制同时执行的线程数量:
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
semaphore.acquire(); // 获取许可
System.out.println(Thread.currentThread().getName() + " 执行任务中");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放许可
}
}).start();
}
输出:
Thread-1 执行任务中
Thread-2 执行任务中
Thread-3 执行任务中
(1秒后)
Thread-4 执行任务中 ...
任意时刻只有 3 个线程在执行任务。
(2)数据库连接池控制
在多线程访问数据库连接池时,可以用 Semaphore 控制连接数量:
Semaphore dbSemaphore = new Semaphore(10);
确保连接不被过度占用,避免数据库过载。
(3)流量限速(接口级别)
每秒允许 N 个请求通过,可在服务网关层用信号量实现动态限流。
(4)多线程同步启动
在并发压测中可通过 Semaphore(0) 控制所有线程同步开始:
Semaphore start = new Semaphore(0);
for (...) {
new Thread(() -> {
try {
start.acquire(); // 等待统一放行
task();
} catch (Exception e) {}
}).start();
}
Thread.sleep(1000);
start.release(threadCount); // 所有线程同时开始
六、面试高频问题与答题模板
| 问题 | 答案要点 |
|---|---|
| Q1:Semaphore 是如何实现的? | 基于 AQS 的共享模式,state 表示剩余许可数,通过 CAS 控制并发。 |
| Q2:Semaphore 与 Lock 的区别? | Lock 是独占锁,只允许一个线程;Semaphore 控制多个线程共享资源。 |
| Q3:Semaphore 的公平模式如何实现? | 通过判断 hasQueuedPredecessors() 确保 FIFO 顺序。 |
| Q4:acquire() 与 tryAcquire() 区别? | 前者会阻塞,后者立即返回布尔结果。 |
| Q5:可以动态增加许可证吗? | 可以通过 release(int n) 增加许可数。 |
| Q6:Semaphore 是否可重入? | 不可重入,一个线程获取许可后需显式释放。 |
| Q7:Semaphore 与 CountDownLatch 区别? | Semaphore 控制“可用数量”,CountDownLatch 用于“等待倒计时”。 |
结语
Semaphore 是并发编程中最经典的限流工具之一,
它通过 AQS + 许可证机制 优雅地实现了“并发访问数量控制”。
无论是线程池控制、接口限流、资源配额,
Semaphore 都能用最小代价实现高效安全的并发管理。
下一篇,我将写——
《ThreadLocal 深入解析:线程副本机制与内存泄漏风险》。
更多推荐



所有评论(0)