Semaphore 超详细完整版讲解
类路径:java.util.concurrent.Semaphore 中文名称:信号量
一、核心概念
通俗理解
停车场模型: 信号量内部维护固定数量停车位(许可 permit)
acquire():开车进场,拿走 1 个许可,没有车位就阻塞等待release():车辆离场,归还 1 个许可
本质:控制同一时间能够执行的线程数量,实现资源限流、资源抢占。
两大模式:
- 共享锁思想(底层基于 AQS 共享模式),多个线程可以同时获取许可
- 分为:公平信号量 / 非公平信号量
对比区分记忆
- CountDownLatch:等待任务完成(门闩)
- CyclicBarrier:线程互相等待集合(栅栏)
- Semaphore:控制并发数量(通行证)
二、全部构造方法
// permits:初始许可数量,非公平模式 public Semaphore(int permits) // fair=true:公平信号量;false:非公平 public Semaphore(int permits, boolean fair)
- permits ≥ 0;permits=0 初始无许可,必须外部 release 才能获取
- 非公平(默认):线程释放许可后,新来线程和等待队列线程竞争许可,性能更高
- 公平模式:严格按照等待队列顺序获取,先等先得,上下文切换多,吞吐量略低
三、完整 API 分类
1. 阻塞获取许可(常用)
// 获取1个许可,无许可则阻塞;可被中断 void acquire() throws InterruptedException // 获取N个许可 void acquire(int permits) throws InterruptedException
2. 不响应中断的阻塞获取(极少用)
void acquireUninterruptibly() void acquireUninterruptibly(int permits)
线程被 interrupt 时不会抛出异常,会继续等待许可,业务尽量避免。
3. 非阻塞尝试获取(推荐生产使用,防止死锁)
// 立刻尝试,拿到返回true,没有直接false,不阻塞 boolean tryAcquire() boolean tryAcquire(int permits) // 带超时尝试获取 boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException boolean tryAcquire(int permits, long timeout, TimeUnit unit)
4. 释放许可
// 归还1个许可 void release() // 归还N个许可 void release(int permits)
⚠️ 重大注意:release 不会校验是不是当前线程 acquire 的!随便哪个线程都能释放,容易出现许可膨胀!
5. 监控工具方法(瞬时快照,不能用于业务判断)
int availablePermits() // 当前剩余可用许可 int drainPermits() // 一次性拿走所有可用许可,返回拿到数量 int getQueueLength() // 获取正在等待许可的线程预估数量 boolean hasQueuedThreads() // 是否存在等待线程 boolean isFair() // 是否公平模式
四、底层 AQS 源码核心逻辑
Semaphore 内部同样有 Sync,继承 AQS:
state:代表当前剩余许可数量
注意和 CountDownLatch 区分: CountDownLatch:state = 剩余需要 countDown 的次数 Semaphore:state = 当前可用通行证
非公平模式核心方法
// 获取许可共享尝试
protected int tryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
// 许可不足 返回-1 进入阻塞队列
if (remaining < 0 || compareAndSetState(available, remaining))
return remaining;
}
}
// 释放许可
protected boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
if (next < current) // 溢出保护
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true;
}
}
流程梳理:
acquire()→tryAcquireShared()- state ≥ 需要许可:CAS 扣减 state,直接执行业务
- state 不足:线程进入 AQS 共享阻塞队列挂起
release()→tryReleaseShared()- CAS 增加 state;成功后唤醒队列头部等待线程
关键点: Semaphore 允许多次 release,state 可以超过初始 permits! 也就是:许可上限不受初始化数值约束,人为释放过多会造成「许可膨胀」,限流失效。
五、基础代码示例
示例 1:简单限流,最多 3 个线程同时执行
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class SemaphoreDemo {
public static void main(String[] args) {
// 3个许可:同一时刻最多3条线程运行
Semaphore semaphore = new Semaphore(3);
// 模拟10个并发请求
for (int i = 1; i <= 10; i++) {
int taskId = i;
new Thread(() -> {
try {
// 获取通行证
semaphore.acquire();
System.out.println("任务" + taskId + "获取许可,开始执行");
TimeUnit.SECONDS.sleep(2);
System.out.println("任务" + taskId + "执行完成,释放许可");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// finally释放,保证一定会归还许可
semaphore.release();
}
}).start();
}
}
}
运行现象:始终最多 3 个任务同时执行。
示例 2:tryAcquire 超时避免永久阻塞(生产标准写法)
Semaphore semaphore = new Semaphore(2);
new Thread(() -> {
boolean acquire = false;
try {
// 最多等待1秒拿许可
acquire = semaphore.tryAcquire(1, TimeUnit.SECONDS);
if (acquire) {
System.out.println("获取许可执行业务");
TimeUnit.SECONDS.sleep(1);
} else {
// 限流兜底策略:直接返回繁忙、降级
System.out.println("获取许可超时,触发限流降级");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (acquire) {
semaphore.release();
}
}
}).start();
✅ 最佳实践:一定要标记是否成功获取许可,只有拿到许可才 release!
六、典型业务使用场景
场景 1:接口 / 资源并发限流(最常用)
限制访问有限资源的并发量:
- 第三方 HTTP 接口调用限流(防止调用方被对方风控)
- 文件读写、FTP 连接、数据库外置连接控制
和线程池区别: 线程池限制线程数量;队列还能堆积任务; Semaphore 限制业务并发数量,和线程池无关,可以跨线程池限流。
场景 2:资源池简易实现
连接池、对象池:许可代表可用资源,acquire 拿资源,release 归还资源。
场景 3:实现读写锁(扩展思路)
可以用 Semaphore 简单模拟 ReadWriteLock:
- 读:获取少量许可,支持并发读
- 写:一次性获取全部许可,排斥其他读写(性能不如 ReentrantReadWriteLock,仅思路)
场景 4:异步任务的批量并发控制
批量同步调用外部服务,控制并发数,防止瞬间打垮下游。
场景 5:实现屏障(拓展:permits=1 可当作互斥锁)
new Semaphore(1) 等价简易独占锁,功能类似 ReentrantLock,但不可重入!
致命坑:同一个线程连续 acquire (1) 两次直接死锁,不要当做锁使用!
七、高频致命坑(面试重点)
坑 1:acquire /release 不配对,许可膨胀
// 危险!没有判断是否拿到许可就直接release
try {
semaphore.tryAcquire(1, TimeUnit.SECONDS);
} finally {
semaphore.release();
}
如果获取失败依然执行 release → state 持续上涨,限流彻底失效。 ✅ 规范:用布尔变量标记,只有 acquire 成功才释放。
坑 2:同一个线程多次 acquire,忘记对应次数 release
acquire(2) 必须 release(2),少释放会导致许可持续泄漏。
坑 3:Semaphore (1) 当成可重入锁使用
不可重入!
sem.acquire(); sem.acquire(); // 同一个线程再次获取 → 永久阻塞死锁
需要重入锁使用 ReentrantLock。
坑 4:生产直接使用 acquire () 无限阻塞
下游持续占用许可不释放 → 线程全部阻塞,服务雪崩; 强制规范:业务代码优先 tryAcquire(time,unit),设计降级策略。
坑 5:随意混用 acquire (N)、release (N)
尽量统一使用 acquire () /release () 单次操作,便于排查许可泄漏问题。
八、公平 vs 非公平信号量怎么选?
- 默认非公平(推荐绝大多数业务)
- 性能高,减少线程上下文切换
- 缺点:可能出现线程饥饿(新来线程抢占许可,老线程长期等待)
- 公平模式
- 严格 FIFO 顺序获取许可
- 适合对等待顺序有强要求场景,并发量大吞吐量下降
九、面试高频对比:Semaphore vs 另外两个工具
| 工具 | 核心作用 | 底层 AQS 模式 |
|---|---|---|
| CountDownLatch | 等待一组任务全部完成 | 共享锁 |
| CyclicBarrier | 线程互相等待集合,循环复用 | ReentrantLock+Condition |
| Semaphore | 控制并发数量、资源限流 | 共享锁 |
面试官经典追问:
Q:线程池 maximumPoolSize 限流 和 Semaphore 限流区别? A:
- ThreadPoolExecutor:限制工作线程数量,任务存在队列堆积;适合内部任务调度。
- Semaphore:限制同时运行的业务并发数,与线程池无关;适合保护外部第三方资源(调用外部接口、文件、中间件),可以快速拒绝请求实现限流降级。
Q:Semaphore 可以实现 CountDownLatch 吗? A:语法可以模拟,但语义不匹配,不推荐。
十、一句话面试背诵总结
Semaphore 是基于 AQS 共享模式的信号量,通过许可数量控制同一时间并发访问资源的线程数,常用来做限流;支持公平 / 非公平模式;开发务必保证 acquire 和 release 成对,优先使用带超时 tryAcquire,防止许可泄漏与线程永久阻塞。
拓展:三者完整区分终极面试话术
- CountDownLatch:老板等员工干完活,一次性
- CyclicBarrier:员工互相等待集合,集齐出发,可循环
- Semaphore:停车场通行证,控制同时进场人数,限流

浙公网安备 33010602011771号