结论:用Semaphore控制并发数,核心是合理设许可证数、acquire()与release()必须成对出现在所有路径(尤其finally中)、避免误用公平模式;任一出错即导致线程卡死或限流失效。

直接说结论:用 Semaphore 控制并发线程数,核心不是“怎么写那两行代码”,而是“许可证数量设多少”“acquire() 和 release() 是否成对出现在所有路径”“是否漏了异常场景”。错一个,就可能卡死线程或限流失效。
为什么 release() 必须放在 finally 块里
常见错误是只在正常流程里调用 release(),一旦业务逻辑抛出异常(比如 IOException、NullPointerException),release() 就被跳过。结果就是许可数永久少 1,后续线程在 acquire() 处无限等待。
必须这样写:
try {
semaphore.acquire();
// 执行业务,可能抛异常
doSomething();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
} finally {
semaphore.release(); // 关键:无论成功失败,都归还
}
Java 8+ 还可考虑 try-with-resources 自定义资源包装,但 Semaphore 本身不支持,得自己封装;Python 的 threading.Semaphore 更麻烦,连中断都不支持,只能靠手写 finally。
Java 中公平模式(fair = true)会显著拖慢吞吐量
构造 Semaphore 时传 true 启用公平模式,看起来更“讲道理”,但实际会让所有等待线程排进 FIFO 队列。新来的线程哪怕刚好有空闲许可,也得排队等前面的人——这违背了限流本意(只要不超过上限,谁来都该能进)。
默认非公平模式更适合大多数场景:
- 吞吐更高,尤其高并发下
- 饥饿问题在限流场景中影响极小(你本来就不希望某线程长期霸占许可)
- 若真需要强顺序控制,应该用
CountDownLatch或队列协调,而不是靠Semaphore的公平性
别为了“心理安慰”开公平模式,除非你明确测出非公平导致了不可接受的调度偏差。
Python threading.Semaphore 没有超时和公平选项,卡住就只能重启
Python 标准库的 threading.Semaphore 只有最简功能:acquire(blocking=True) 会一直等,acquire(blocking=False) 立即返回布尔值,没有超时参数,也不支持公平调度。
这意味着:
- 一旦某个线程在
acquire()卡住(比如因死锁或逻辑错误没走到release()),整个信号量就“冻住”了 - 无法用
tryAcquire(timeout=...)做防御性等待,得自己轮询 + 计时,但忙等浪费 CPU - 想用协程?换
asyncio.Semaphore;想跨线程精细调度?引入trio或curio
简单脚本无所谓,生产服务里用 Python 做并发限流,强烈建议避开 threading.Semaphore,直接上异步方案。
许可证数量(permits)到底该设多少
这是最容易被忽略、也最致命的一点:粒度错了,限流就白做了。
比如你初始化 new Semaphore(5),但没想清楚这“5”对应什么:
- 是数据库连接池最大连接数?那它必须和连接池配置一致,否则要么压垮 DB,要么闲置资源
- 是某个外部 HTTP 接口的 QPS 上限?那就得结合平均响应时间估算并发请求数(例如 100 QPS / 平均 200ms = 约 20 并发)
- 是文件句柄数?那得看 OS 限制和业务打开/关闭频率
别凭感觉写个数字。上线前至少做一次压测,观察 semaphore.availablePermits() 和 semaphore.getQueueLength(),确认许可耗尽时的行为符合预期——是平稳排队,还是大量请求超时失败?










