semaphore不是锁,而是控制同时访问线程数量的许可证计数器;它无所有权概念,acquire与release可跨线程调用,需严格配对且置于finally中以防泄漏。

为什么 Semaphore 不是锁,但常被误当锁用
Semaphore 控制的是「同时允许多少个线程通过」,不是「谁有资格独占资源」。它没有所有权概念,acquire() 和 release() 可以由不同线程调用 —— 这和 ReentrantLock 严格要求同一线程 unlock 有本质区别。误把它当锁用,比如在 try-finally 里漏掉 release(),或跨线程释放,会导致许可数错乱、资源永久阻塞。
常见错误现象:acquire() 后程序卡住不动,日志没报错,但后续线程再也拿不到许可;或者看似正常运行,但并发量远低于预期(实际许可被提前耗尽)。
- 务必确保每次
acquire()都配对release(),推荐放在 finally 块中 - 不要在未成功
acquire()的情况下调用release()(会非法增加许可数) - 若需“可中断获取”,用
acquireInterruptibly();若需“带超时”,用tryAcquire(long, TimeUnit)
Semaphore 初始化时设 fair = true 有什么实际影响
默认构造(new Semaphore(permits))是非公平的:新线程可能插队,抢走刚释放的许可,导致某些线程长期饥饿。设 fair = true 后,acquire() 按 FIFO 排队,吞吐略低但行为更可预测。
适用场景:你观察到某类任务(如定时批处理线程)总是等很久才执行,而短任务持续抢占,这时开 fair 更稳妥;高吞吐 API 网关类服务通常保持非公平,默认即可。
- 公平模式下,
acquire()调用本身可能触发线程挂起+唤醒,比非公平多一次 CAS 失败开销 - 即使开启 fair,也不能保证「绝对准时」——只是排队顺序确定,不解决系统负载过高导致的延迟
- 测试公平性是否生效,可启动 N 个线程连续
acquire(),再逐个release(),检查它们唤醒顺序
如何安全地动态调整许可数量
Semaphore 创建后不能直接改总许可数,但可通过 reducePermits(int) 和 release(int) 组合实现增减。注意:reducePermits() 是「偷偷扣掉」,不阻塞也不校验当前持有者;release(int) 是「偷偷加回」,哪怕没人 acquire() 过也能加。
典型用途:根据 CPU 使用率或队列积压程度自动扩缩容连接池/任务槽位。
- 减少许可:先调
semaphore.reducePermits(delta),再等现有持有者自然release(),总占用会逐步下降 - 增加许可:直接
semaphore.release(delta)即可,新线程立刻能拿到 - 避免竞态:增减操作本身是线程安全的,但业务逻辑(如判断是否该扩容)需自行同步,别在无锁条件下读指标+调用增减
和 ThreadPoolExecutor 的核心线程数控制有何区别
Semaphore 控制的是「逻辑并发度」,不管理线程生命周期;ThreadPoolExecutor 的 corePoolSize 控制的是「保底活跃工作线程数」。两者定位不同:前者像水龙头阀门(限制流量),后者像雇佣合同(保障最低人力)。
容易混淆的点:用 Semaphore 包裹线程池提交逻辑,以为就能限流 —— 实际上只是让提交动作排队,并未阻止线程池内部创建/复用线程。真要控并发执行数,应把 Semaphore 放在任务 run() 开头,而非 execute() 外围。
- 组合用法:线程池负责线程复用,
Semaphore在每个任务开头acquire(),确保最多 N 个任务真正并行执行 - 性能差异:线程池调度开销固定;
Semaphore在高争用时会有更多自旋/CAS,但无上下文切换成本 - 监控难点:
Semaphore没有内置指标,得靠availablePermits()+ 自定义计数器推断使用率
最易忽略的是许可泄漏:比如任务抛异常后跳过 finally 块,或异步回调里忘记 release()。一旦发生,许可数持续减少,系统会悄无声息地降级为串行。上线前建议加一个定期 dump availablePermits() 的健康检查点。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











