semaphore适合单机场景下限制瞬时并发数,如控制调用第三方服务的线程数;它不控qps或时间窗口,需全局唯一、精准嵌入临界区、try-finally释放、带超时tryacquire,并避开跨线程释放等陷阱。

Java信号量(Semaphore)适合在单机场景下对核心业务接口做并发数限制,比如控制同时调用第三方支付、风控或发券服务的线程数。它不控QPS、不控时间窗口,只管“此刻最多几个线程在跑”,语义清晰、开销极低,但必须用对方式,否则容易失效或拖垮系统。
明确适用边界:什么时候该用,什么时候不该用
信号量只在以下情况真正有效:
- 部署是单JVM实例,或你已手动均分限流值(例如集群共6台机器,上游系统最大支持30并发,则每台配
Semaphore(5)) - 目标是防止下游资源被压垮,比如数据库连接池满、HTTP客户端线程耗尽、或对方API明确要求“并发≤5”
- 不需要按用户ID/租户/URL路径做差异化限流,也不需要滑动窗口、分钟级统计等复杂策略
- 业务逻辑本身是同步阻塞型(如HTTP同步调用、JDBC查询),不是纯异步回调或CompletableFuture链式调用
正确初始化与全局唯一性
必须确保整个应用生命周期内只存在一个Semaphore实例,否则每个新对象都是一套独立计数器,限流就形同虚设。
- Spring项目中声明为
@Bean,作用域@Scope("singleton")(默认就是) - 避免在Controller方法里每次new Semaphore(3),那等于没限
- 许可数设多少?参考下游系统文档或压测结果,宁可略保守(如对方说“支持10并发”,你设8)
临界区嵌入要精准,且必须try-finally保障释放
不要把acquire()放在Controller入口,而应紧贴真实资源调用前——比如在发起HTTP请求之前获取许可,而不是在参数校验之后。
- 推荐写法:
try { semaphore.tryAcquire(1, 100, TimeUnit.MILLISECONDS); }——带超时,防毛刺排队 - 获取失败时返回
429 Too Many Requests,而不是让线程卡住 - release()必须写在finally块里,哪怕业务抛出RuntimeException、NPE或远程调用超时,也要归还许可
- 禁止在catch块里release——成功执行后就没地方还了;也禁止放在try末尾——异常会跳过它
避开常见陷阱
这些错误会导致许可证泄漏、线程池耗尽或限流失效:
- 跨线程释放:在主线程acquire,却在CompletableFuture回调里release → JVM报
IllegalMonitorStateException或静默失败 - 未处理中断:
acquire()可能抛InterruptedException,需恢复中断状态:Thread.currentThread().interrupt(); - 用错方法:想快速失败却用了无参
tryAcquire(),结果突发流量全被拒绝;想阻塞等待却漏了超时,导致Tomcat线程池满 - 忽略公平性:高并发下,默认非公平模式可能导致某些请求长期饥饿,可考虑构造时传
true启用公平队列
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











