父子任务共用线程池导致死锁
父子任务共用线程池导致死锁的核心原因是资源竞争与循环等待。当父任务在执行过程中提交子任务到同一线程池,并等待子任务完成时,如果线程池的线程资源被父任务完全占用,子任务无法获取线程执行,父任务又因等待子任务结果而无法释放线程,最终形成死锁。
原因分析:
- 线程池资源竞争
父任务占用了线程池的所有线程,子任务因无可用线程而被放入队列等待。 - 父任务等待子任务结果
父任务通过Future.get()等方法阻塞等待子任务完成,而子任务无法执行(因线程被父任务占用)。 - 循环依赖
子任务需要父任务释放线程才能执行,父任务需要子任务完成才能释放线程,形成死锁。
1 | import java.util.concurrent.*; |
过程分析:
假设线程池大小为2(如示例所示):
- 初始状态
Parent-1占用线程1Parent-2占用线程2
- 父任务提交子任务
Parent-1提交Child-1→ 进入任务队列Parent-2提交Child-2→ 进入任务队列
- 父任务等待子任务
Parent-1调用childFuture.get()→ 阻塞等待Child-1执行Parent-2调用childFuture.get()→ 阻塞等待Child-2执行
- 死锁形成
- 线程池中所有线程(线程1、2)都被父任务占用且处于阻塞状态
- 子任务(
Child-1,Child-2)在队列中等待空闲线程 - 但父任务不释放线程 → 子任务永远得不到执行 → 父任务永远等不到结果
这种情况下,只有当工作队列是有界的,且父任务在阻塞前提交多个子任务使工作队列被占满,从而创建新线程才有可能让死锁解开。(有一种父子任务共用线程池不会出现死锁的情况——队列是 SynchronousQueue 这种无存储功能的)
在这种父子共用线程池的场景,建议有一个兜底的操作——在父任务在等待子任务结果时设置一个超时时间。虽然这个不能完全避免死锁的出现,但是可以避免永久死锁。
1 | try { |
那怎么完全避免这种死锁呢:
- 父任务和子任务用独立线程池;
- 避免阻塞等待,使用
CompletableFuture异步回调
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 ECAMT35 的地下室!