Semaphore信号量实战案例全解析
目录导读
- Semaphore是什么?为什么需要它?
- 核心原理:从“许可证”到“流量闸门”
- 经典案例一:数据库连接池限流
- 经典案例二:电商秒杀系统的抢购保护
- 经典案例三:文件上传服务并发控制
- 高频面试问答(含源码级解析)
- 性能调优与避坑指南
- 何时用Semaphore,何时用其他并发工具
Semaphore是什么?为什么需要它?
在多线程编程中,我们经常遇到“资源有限,但请求无限”的困境,数据库连接池最多只有10个连接,但有100个线程同时请求;秒杀系统瞬间涌入10万用户,但库存只有100件。

如果不对这些并发请求加以控制,轻则系统响应变慢,重则直接崩溃。Semaphore(信号量) 就是Java并发包中专门用来解决这类“限流”问题的工具,它通过维护一组许可证(Permits),控制同时访问特定资源的线程数量。
与synchronized或Lock(它们只允许一个线程进入临界区)不同,Semaphore允许N个线程同时进入,非常适合“资源池化”场景。
核心原理:从“许可证”到“流量闸门”
Semaphore内部维护一个计数器(state),每次acquire()获取许可时减1,release()归还许可时加1,当计数器为0时,后续线程阻塞等待。
关键方法:
acquire()/acquire(int permits):阻塞获取许可(可响应中断)tryAcquire():非阻塞尝试获取,失败立即返回falserelease()/release(int permits):释放许可availablePermits():查看当前剩余许可数
两种模式:
- 公平模式:FIFO顺序发放许可,避免线程饥饿(构造参数
new Semaphore(3, true)) - 非公平模式:抢占式,吞吐量更高(默认)
经典案例一:数据库连接池限流
场景描述:假设系统维护一个固定大小的数据库连接池(比如5个连接),多个业务线程需要并发访问数据库,如果不限制,可能创建过多连接导致数据库崩溃。
实现方案:
public class DBPool {
private final Semaphore semaphore;
private final List<Connection> connections;
public DBPool(int poolSize) {
this.semaphore = new Semaphore(poolSize, true);
this.connections = new ArrayList<>();
// 初始化连接...
}
public Connection getConnection() throws InterruptedException {
semaphore.acquire(); // 获取许可,没有则等待
return connections.remove(0); // 从池中取连接
}
public void returnConnection(Connection conn) {
connections.add(conn);
semaphore.release(); // 归还许可
}
}
效果:无论多少线程请求,同时只有5个线程能获得连接,其余线程优雅等待。
经典案例二:电商秒杀系统的抢购保护
场景描述:秒杀活动开始瞬间,大量请求涌入,后端服务需要防止订单系统被高并发打垮,同时保证库存扣减原子性。
实现方案(伪代码):
// 在秒杀接口入口处加入Semaphore限流
Semaphore seckillLimit = new Semaphore(100); // 同时最多处理100个秒杀请求
public Result seckill(Long userId, Long productId) {
if (!seckillLimit.tryAcquire()) {
return Result.error("系统繁忙,请稍后再试"); // 快速失败,保护系统
}
try {
// 1. 校验库存
// 2. 扣减库存(Redis或DB原子操作)
// 3. 创建订单
return Result.success();
} finally {
seckillLimit.release(); // 释放许可
}
}
关键优化:使用tryAcquire而非acquire,避免请求大量阻塞堆积,而是快速返回“系统繁忙”提示,保护核心业务。
经典案例三:文件上传服务并发控制
场景描述:文件上传服务需要限制同时处理的文件数量,防止内存溢出和带宽耗尽。
实现方案:
public class UploadService {
private static final Semaphore UPLOAD_LIMIT = new Semaphore(20); // 同时最多20个上传任务
public void handleUpload(UploadRequest request) {
if (UPLOAD_LIMIT.tryAcquire(2, TimeUnit.SECONDS)) { // 等待最多2秒
try {
processFile(request); // 执行上传
} finally {
UPLOAD_LIMIT.release();
}
} else {
throw new BusyException("当前上传任务过多,请稍后重试");
}
}
}
此案例亮点:结合tryAcquire(timeout),在限定时间内获取许可,超时则快速失败,保证用户体验。
高频面试问答(含源码级解析)
Q1:Semaphore与CountDownLatch有什么区别?
- Semaphore:控制同时访问资源的线程数量,许可可复用(获取-归还-再获取)
- CountDownLatch:等待N个事件完成后放行,计数只能做减法,不能重复使用
- 一句话记忆:Semaphore管“同时能有几个”,CountDownLatch管“什么时候开始”
Q2:Semaphore 获取许可后,如果线程异常退出,会有什么问题?
如果线程在acquire()之后抛出异常,而没有在finally中release(),会导致许可泄漏,最终所有线程阻塞,因此必须使用try-finally结构。
Q3:Semaphore能实现互斥锁吗?
可以。new Semaphore(1) 即为二元信号量,效果等同于synchronized,但不保证线程所有权(可由其他线程释放),更适合“信号传递”场景。
Q4:公平模式一定比非公平模式好吗?
不一定,公平模式避免了线程饥饿,但会降低系统吞吐量,对于高并发场景且对延迟不敏感,推荐非公平模式;对于必须保证请求顺序的场景(如打印任务),推荐公平模式。
性能调优与避坑指南
常见坑位:
- 忘记释放许可 → 死锁风险极高,必须用
finally - 释放过多许可 → 超出初始容量,导致限流失效
- 在循环中acquire → 可能导致线程长时间等待,注意中断响应
- 与事物结合时 → 事务提交后释放许可,否则事务期间资源被占用
调优建议:
- 初始许可数设置为预估并发峰值的80%,留20%缓冲
- 用
tryAcquire替代acquire,配合超时策略 - 监控
availablePermits()和queueLength(),动态调整池大小 - 在高并发短任务场景下,可以通过
Semaphore与ThreadPoolExecutor组合使用,形成“双层限流”
何时用Semaphore,何时用其他并发工具?
| 场景 | 推荐工具 | 原因 |
|---|---|---|
| 限制同时访问资源数 | Semaphore | 天然支持多许可 |
| 互斥访问 | synchronized/Lock | 保证所有权,可重入 |
| 多线程等待单点完成 | CountDownLatch | 一次性放行 |
| 循环屏障同步 | CyclicBarrier | 可重置,多线程互相等待 |
| 读写分离 | ReentrantReadWriteLock | 性能优化于读多写少 |
| 限流+快速失败 | Semaphore + tryAcquire | 保护系统稳定 |
最后核心要义:Semaphore是“流量阀门”,不是“业务逻辑”,正确使用它,能让你在千万级并发面前保持系统丝滑流畅;错误使用,则可能引入隐性死锁,记住“acquire必须配对release”,并且优先使用tryAcquire超时版本,这是生产环境无数血泪教训总结出的铁律。
延伸思考:在Spring Boot中,如何用
@Async+ Semaphore实现方法级限流?欢迎在评论区留言讨论你的生产级实现方案。