semaphore仅控制并发数而非频率,限频需结合超时、计时或调度;必须用tryacquire带超时降级,release须在finally中,许可数应基于下游真实容量,非公平模式更优。

不能直接用 Semaphore 控制“频率”(如每秒 N 次),它只管并发数,不管时间窗口。想限频必须配合超时、计时或外部调度。
acquire() 会卡死,别在响应敏感服务里无脑用
调用 acquire() 时,线程会一直阻塞直到拿到许可——如果下游资源长期不可用,或者某个线程持有许可不放(比如没进 finally),整个线程池可能被拖垮。尤其在 Web 接口、定时任务这类对延迟敏感的场景,等于埋雷。
- 必须改用带超时的
tryAcquire(long timeout, TimeUnit),超时后主动降级(返回 429、走缓存、抛业务异常) - 超时值不能拍脑袋定:建议设为下游 P95 响应时间的 1.5~2 倍,例如 Redis 平均耗时 20ms,P95 是 80ms,超时可设 120~160ms
- 别用
acquireUninterruptibly():它连Thread.interrupt()都不响应,JVM 关闭时可能 hang 住
许可数设成多少才不压垮下游
不是看线程池大小,也不是拍个整数,而是查清目标资源的真实容量上限。比如:
- 数据库连接池最大连接数是 10 →
Semaphore(10),再加 10% 余量就是Semaphore(11) - 第三方 API 明确限制 “2 QPS”,但
Semaphore不感知时间,所以得结合调度:用ScheduledExecutorService每 500ms 放行最多 1 个请求,再配Semaphore(1)防并发突刺 - 云数据库连接池自动扩缩容 →
Semaphore失效,该换Resilience4j RateLimiter这类支持滑动窗口的限流器
release() 忘写或写错位置,系统会悄无声息地假死
许可泄露是最隐蔽的生产事故源头。现象是:初期正常,压测半小时后所有请求卡在 acquire(),监控显示线程全在 WAITING 状态,availablePermits() 持续为 0。
- 唯一安全写法:
acquire()后立刻进try,release()必须在finally块里,哪怕业务逻辑只有一行 - 禁止在
if分支里写release(),也禁止依赖try-with-resources(Semaphore不实现AutoCloseable) - 同一个线程多次
acquire(),必须对应次数调用release();跨线程释放虽合法,但极易引发计数混乱,尽量避免
公平模式(fair = true)在高并发下反而坏事
设 new Semaphore(3, true) 看似更“稳妥”,实际会让性能下降 20%+,且无法彻底解决饥饿——AQS 的公平队列只是减少插队,并非严格 FIFO。
- 默认非公平模式更适合大多数场景:刚释放的许可能被新线程快速抢到,吞吐更高
- 只有当明确观察到某些线程长期拿不到许可(比如日志里反复出现某线程 ID 等待超 5s),才考虑开公平模式
- 开了公平模式后,务必压测对比吞吐变化,有些服务 QPS 直接掉 30%
真正难的不是写对那几行 acquire 和 release,而是搞清你要限的到底是“同时跑几个”,还是“一秒钟最多来几次”。前者用 Semaphore 刚好,后者它根本不管——这时候硬套只会让问题更难排查。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











