Java -- 线程池使用不当引发的死锁
前段时间线上写了一个DAG调度框架,整个DAG依赖关系以及执行依赖JDK8提供的CompletableFuture,运行一段时间后,生产出现了业务死锁问题,经过一番排查后,发现是使用不当造成,遂记录该篇文章希望对你有帮助。
问题场景回顾
主要业务是发起DAG图的执行,如下图所示,用户发起一个DAG任务请求,该请求到达Planner后,异步去启动一个DAG任务,当执行到DAG时,该DAG会异步遍历整个图,然后阻塞的获取最后结果。
原有业务系统太过于复杂,因此我将相关逻辑提取了出来,简述为以下代码表示,其中关键链路如下:
- 用户对一个同步操作发起异步调用
- 该同步操作本质上是异步调用,同步等待
- 两者使用了同一个线程池
1 | public class Test { |
该代码在运行一段时间后,发生了死锁,具体现象为:ThreadPool中queue在不停的累计,但所有的core thread全部处于WAITING状态。也就是说线程池中的每一个线程都在等待某一个信号,从而导致queue中的任务无法消费。
问题原因
从现象来看,问题的原因是线程池中core线程执行的任务被阻塞了,一直无法完成,所以新的任务不停的往queue中累积。那为什么线程池中的core线程会被阻塞?
首先找出与线程池相关的代码,确定线程池中执行了哪些任务:
1 | // 线程池执行了Test::work这个任务 |
比较特殊的是 Test::work 这个任务的完成依赖于Test::workInnerTask,那么需要Test::workInnerTask执行完毕Test::work才能完成,然而线程池是先将Test::work放入到queue,再将Test::workInnerTask放入到queue,那么只要前者足够多到将core线程池全部占满,就会导致后者一直无法完成,前者由于等待后者也无法完成,造成死锁。
解决问题
原因定位到后,解决思路就很清晰了。
第一种方式,将Test::work放入到另一个独立的线程池中执行。两边线程池互不影响,那么在一个queue上就不会产生阻塞。
第二种方式,去除Test::work中的get()阻塞,让其返回CompletableFuture,也就是异步调用就全链路执行异步,没必要中间出现同步代码。
总结
这个简化版案例代码,可以很容易找到具体原因,但是在复杂业务系统中,调用链路错综复杂,由于线程池的复用很容易引发类似问题,如何才能避免这种问题呢?
博主想了许久,没有找到靠谱的结论,不过有两点准则在日常开发中可以参考:
1.业务系统尽量不要使用公共线程池,不同的业务使用不同的线程池隔离
2.阻塞操作想要变异步时,使用单独线程池,而不是公共线程池
如果您有更好的建议,欢迎分享。
- 版权声明: 感谢您的阅读,本文由屈定's Blog版权所有。如若转载,请注明出处。
- 文章标题: Java -- 线程池使用不当引发的死锁
- 文章链接: https://mrdear.cn/posts/java_threadpool_completablefuture.html