必须用try-finally确保release()执行,否则异常时许可泄漏导致线程永久阻塞;Semaphore不自动归还许可,acquire与release必须严格配对;与ThreadPoolExecutor混合使用需注意限流逻辑差异。

Java里用Semaphore限制并发下载线程数,到底怎么写才不漏许可
直接说结论:Semaphore必须和try-finally绑定,否则一旦下载出错或中断,许可没释放,后续所有线程都会卡死在acquire()上。
常见错误是只在成功路径里调用release(),但网络请求可能抛IOException、InterruptedException,甚至JVM被杀——这些场景下许可永远回不来。
-
acquire()前不检查是否已中断,可能阻塞到天荒地老(尤其在线程池里) - 用
acquireUninterruptibly()看似省事,实则掩盖中断信号,影响上层线程管理 - 初始化
Semaphore时传false(非公平模式),高并发下可能造成某些线程饿死,建议显式传true
正确姿势:
semaphore.acquire(); // 可能抛 InterruptedException
try {
downloadFile(url);
} finally {
semaphore.release(); // 保证释放,哪怕中途异常
}
下载任务抛异常时,Semaphore许可会不会自动归还
不会。Semaphore完全无感知——它不是锁,不和栈帧或异常传播挂钩。你申请多少次acquire(),就必须配对多少次release(),少一次,计数就少一,池子就缩一格。
立即学习“Java免费学习笔记(深入)”;
典型翻车场景:
你接手一个需求时,是不是经常这样:"改一个小字段 → 结果把不相关的代码也改了"、"需求说得很模糊 → 做着做着发现跑偏了"、"数据库加个字段 → 上线才发现漏了数据兼容"、"做了个新功能 → 一个月后自己都忘了当时怎么设计的"、"写到一半被中断 → 再回来不知道从哪继续";这个 Skill 就是专门解决这些问题的。 它会自动探测你的项目环境(JDK、Spring Boot、ORM、数据库、前端框架),记在
- 下载中遇到
SocketTimeoutException,没进finally块(比如用了return提前退出) - 用
CompletableFuture异步下载,把release()丢进thenRun(),结果Future被取消,回调根本没执行 - 多个
acquire()嵌套(比如重试逻辑里反复acquire()),但只release()一次
验证方法:下载任务里手动抛个RuntimeException,观察后续任务是否全部阻塞——如果阻塞了,说明有许可泄漏。
Semaphore和ThreadPoolExecutor的限流逻辑冲突吗
会,而且很隐蔽。如果你既用newFixedThreadPool(5),又用new Semaphore(5),表面看都是“最多5个”,但实际行为完全不同:
- 线程池限制的是“同时运行的线程数”,但线程空闲时仍占着资源;
Semaphore限制的是“正在执行下载逻辑的并发数”,线程可以复用、等待、甚至做其他事 - 线程池满后新任务排队在
workQueue里,而Semaphore排队在许可队列里——前者可能OOM(队列太大),后者只是阻塞 - 混合使用时,若线程池大小 Semaphore许可数,等于白设;若线程池远大于许可数,大量线程会卡在
acquire(),浪费调度开销
推荐做法:关掉线程池的“固定上限”幻觉,用newCachedThreadPool()或自定义ThreadPoolExecutor,把并发控制权完全交给Semaphore。
下载器重启或超时后,如何避免Semaphore状态残留
Semaphore本身无状态残留问题——它只是个整数计数器,不记录谁拿了许可、何时拿的。真正麻烦的是外部状态没清理干净:
- HTTP客户端连接没关闭(如
CloseableHttpClient未close()),导致端口耗尽,看起来像限流失效 - 下载临时文件没删,磁盘满了,后续任务全失败,误以为是
Semaphore没起作用 - 重试逻辑里重复
acquire()但没判断是否已持有许可(Semaphore不支持“可重入”,多次acquire()就要多次release())
关键点:限流只是控制“同时发起几个请求”,不解决下游稳定性。务必确保每个下载任务的收尾动作(关连接、删临时文件、记录日志)都在finally里跑完,否则Semaphore再准也没用。

















