直接 new semaphore(1) 不等于锁,因其非公平、不绑定线程、可重复 acquire/release,仅计数许可证;需用 new semaphore(1, true) 模拟单线程独占,限流场景须用 tryacquire 而非 acquire。

为什么直接 new Semaphore(1) 不等于锁
因为 Semaphore 默认是非公平的,且它不绑定线程——同一个线程可以多次 acquire(),也能重复 release(),这和 synchronized 或 ReentrantLock 的线程持有模型完全不同。它只数“许可证数量”,不管谁拿的。
常见错误是拿它当可重入锁用,结果出现:线程 A acquire() 两次,B 却能抢到剩余许可;或者忘记 release() 导致许可证永久泄漏。
- 务必在
try块里acquire(),finally里release() - 如果要模拟“单线程独占”,得配
new Semaphore(1, true)(公平模式),否则高并发下可能饥饿 -
acquireUninterruptibly()看似省事,但会掩盖中断意图,服务治理场景慎用
限流器里 acquire() 和 tryAcquire(long, TimeUnit) 怎么选
真实限流必须响应超时或中断,不能死等。比如 HTTP 接口限流,后端等待 5 秒才拒绝请求,用户体验已经崩了。
acquire() 是阻塞式,没超时,适合内部资源池(如数据库连接池初始化);而限流场景几乎全得用 tryAcquire(long, TimeUnit)。
- 返回
false表示没抢到许可,应立即返回 429 或降级逻辑 - 注意单位:传
100, TimeUnit.MILLISECONDS,不是0.1或字符串 - 别用
tryAcquire(0, TimeUnit.SECONDS)模拟“非阻塞”——它等效于availablePermits() > 0,但语义不清,易被误读
信号量初始化值设多少?别只看 QPS
设成“平均并发数”是常见误区。实际要结合任务耗时波动、下游容忍度、失败重试行为来算。
例如接口平均响应 200ms,目标吞吐 50 QPS,理论最小并发 ≈ 50 × 0.2 = 10。但如果下游偶发卡顿到 2s,那瞬间堆积可能冲垮许可数为 10 的信号量。
- 压测时观察
getQueueLength():持续 > 0 说明许可太紧,该调大 - 若业务有重试(如三次调用),实际并发可能达理论值 × 3,需预留余量
- 避免硬编码:
new Semaphore(Integer.parseInt(System.getProperty("semaphore.permits", "20")))
共享信号量跨 JVM 无效,别踩分布式坑
Semaphore 是纯内存状态,JVM 崩了就清零,多实例部署时各管各的,完全起不到全局限流作用。
常见翻车现场:本地测试 OK,上线后流量打穿所有节点;或者用 Spring Bean 单例管理一个 Semaphore,误以为能控住整个服务集群。
- 单机限流(如防突发自调用)可用
Semaphore,但必须明确标注 “JVM-scoped” - 要跨进程限流,得换
Redis + Lua(如令牌桶)、etcd临时租约,或专用限流组件(Sentinel、Resilience4j) - 哪怕只用
AtomicInteger做计数,也比误用Semaphore更贴近分布式现实
真正难的从来不是写对一行 semaphore.acquire(),而是想清楚这个“10”到底是拍脑袋定的,还是从 P99 延迟、重试率、下游水位一起推出来的。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











