new Semaphore(1) 不等于 synchronized,因前者是基于AQS的可跨线程释放的共享锁,后者是JVM层必须同栈帧成对使用的独占锁;且Semaphore阻塞时线程状态为WAITING而非BLOCKED。

为什么 new Semaphore(1) 不等于 synchronized
因为 Semaphore 是基于 AQS 的共享锁,而 synchronized 是 JVM 层的独占锁;前者可跨线程、跨方法释放(比如 acquire 在线程 A,release 在线程 B),后者必须成对出现在同一个栈帧里。实际用错时常见现象是:线程卡死在 acquire(),但没任何线程调用 release() —— 尤其在异常路径下忘记 finally 释放。
实操建议:
- 永远把
acquire()放在try前,release()放在finally块里 - 避免用
new Semaphore(1)替代synchronized,除非你需要超时、中断响应或跨上下文释放 -
Semaphore构造函数第二个参数fair默认为false,非公平模式下可能造成线程饥饿,高一致性要求场景应显式设为true
acquire() 阻塞时线程状态到底是 WAITING 还是 BLOCKED
是 WAITING。因为 Semaphore 底层调用的是 LockSupport.park(),而非进入 monitor 锁竞争队列。这会影响线程 dump 分析:看到 java.lang.Thread.State: WAITING (parking) 就该想到可能是 Semaphore、CountDownLatch 或 Condition 导致的挂起,而不是锁竞争。
关键区别:
立即学习“Java免费学习笔记(深入)”;
-
BLOCKED:在 synchronized 或ReentrantLock.lock()等待获取 monitor 或 AQS 同步队列头节点时出现 -
WAITING:调用acquire()、await()、join()等无超时阻塞方法后进入 - 若用了
acquireUninterruptibly(),即使被中断,线程仍保持WAITING,不会抛InterruptedException
如何安全地限制数据库连接池之外的外部资源访问
比如调用第三方 HTTP 接口,每秒最多 10 次请求。这时不能直接用连接池自身的限流(如 HikariCP 的 maximumPoolSize),而要用 Semaphore 包裹调用逻辑。
注意点:
- 信号量实例必须是共享的单例,不能每次请求都 new 一个
- 超时控制优先用
tryAcquire(long timeout, TimeUnit unit),避免无限等待拖垮整个服务 - 如果接口返回 429(Too Many Requests),应主动
release()并补偿令牌,否则下次请求仍会被拒绝 - 不要在
acquire()和实际调用之间插入耗时操作(如日志序列化、参数校验),否则信号量保护的时间窗口变大,失去限流意义
permits 数量设太大或太小会有什么实际影响
设太小(如 new Semaphore(2) 用于处理 100 QPS 的 API)会导致大量线程排队,getQueueLength() 持续增长,CPU 花在 AQS 队列维护上,吞吐反而下降;设太大(如 1000)则失去限流作用,还可能掩盖下游真实容量瓶颈。
更隐蔽的问题:
- 初始 permits 设为 0 可实现“冷启动开关”——后续用
release(n)动态放开,但要注意没有线程能acquire()成功,直到第一次release() -
availablePermits()返回的是当前剩余数,不是初始值,不能靠它判断是否“刚初始化” - 调用
drainPermits()会清空所有可用令牌并返回数量,适合做瞬时熔断,但之后必须配对release(n)才能恢复,否则永久不可用
真正难的是根据下游 SLA(比如平均响应 200ms,错误率 getQueueLength() 和 hasQueuedThreads(),而不是拍脑袋定个数字。










