线程池用多了总会出现些诡异问题,特别是当任务间的关系比较复杂时,经常会出现让你想象不到问题,比如这次出现的这个问题。
突然间,系统出现大量报警,具体信息如下:
图片
从抛出的异常可知,提交量较大导致线程池资源被耗尽,从而触发了线程池的拒绝策略,直接抛出了 RejectedExecutionException。
开始的时候,小艾认为等高峰流量过去后,系统便能恢复正常。可出乎意料的是,系统一直没有恢复,那么流量已经将至个位数,请求也是 100% 失败,同时该节点的大量后台任务都出现异常。没有办法,为了快速止损,不得已对异常节点进行重启,系统随之恢复正常,日志输入如下:
图片
其他的后台任务也恢复正常。
惊魂初定的小艾找到出问题的代码如下:
@GetMapping("syncSubmit")public RestResult<String> syncSubmit(String taskName){ this.executeService.submit(new ParentTask()); return RestResult.success("提交成功");}class ParentTask implements Callable<Boolean>{ @Override public Boolean call() throws Exception { Future<A> aFuture = executeService.submit(new FetchAChildTask()); doSomeThing(500); Future<B> bFuture = executeService.submit(new FetchBChildTask()); doSomeThing(500); C c = buildC(aFuture.get(), bFuture.get()); Future<Boolean> cFuture = executeService.submit(new SaveCChildTask(c)); return cFuture.get(); }}
代码的逻辑非常简单,核心流程如下图所示:
图片
核心流程为:
逻辑非常简单,唯一的复杂点在于:==多处任务提交使用了统一线程池。==
【背景】考虑到线程池是系统中最宝贵的资源,公司“大牛”封装了一个全局的 GlobalExecuteService 服务,并制定规范要求所有异步任务统一使用 GlobalExecuteService 来完成。如果需要构建自己的线程池,需要向他提交审批,只有在审批后才能创建新的线程池。
线程池处于什么状态?为什么所有异步任务都无法提交?
这是一个比较烧脑的问题,单盘逻辑没有什么头绪,没有办法只能将现场 dump 下来进行分析。
第一个问题:线程池线程都处于什么状态,线程栈信息如下:
图片
从日志中可知:
接下来,需要进一步确认 FutureTask#10 具体处于什么状态,从内存堆中找到 FutureTask#10 对象,详细信息如下:
图片
从日志中可以看出:
排查到这里,真相浮出水面:GlobalExecuteService 中的线程,正在等待 GlobalExecuteService 阻塞队列的任务完成。
具体如下图所示:
图片
线程池中的所有工作线程都在等待阻塞队列的任务完成,由于没有可用的工作线程,阻塞队列中的任务永远都不会被执行。
这就是典型的死锁!!!
费了老大劲终于定位问题,解决思路也就变的明了:不要向自己运行的线程池提交任务。
图解如下:
图片
线程池不会向自己提交任务,而是将任务提交到其他线程池。
问题修复变的简单,我们需要:
具体代码如下:
@Autowiredprivate GlobalExecuteService executeService;// 创建新的线程池服务@Autowiredprivate SubExecuteService subExecuteService;@GetMapping("syncSubmit")public RestResult<String> syncSubmit(String taskName){ this.executeService.submit(new ParentTask()); return RestResult.success("提交成功");}class ParentTask implements Callable<Boolean>{ @Override public Boolean call() throws Exception { log.info("Begin to Run Parent Task"); // 向新的线程池服务提交任务 Future<A> aFuture = subExecuteService.submit(new FetchAChildTask()); doSomeThing(500); // 向新的线程池服务提交任务 Future<B> bFuture = subExecuteService.submit(new FetchBChildTask()); doSomeThing(500); C c = buildC(aFuture.get(), bFuture.get()); // 向新的线程池服务提交任务 Future<Boolean> cFuture = subExecuteService.submit(new SaveCChildTask(c)); Boolean result = cFuture.get(); log.info("End to Run Parent Task"); return result; }}
手工拆分线程池确实能解决这个场景的问题,但由于 GlobalExecuteService 服务已经使用很长时间,任务间的关系错综复杂,很难一次性排查并修复所有问题,同时随着逻辑的变化未来仍旧会出现类似的问题。
那最佳方案是什么?
让 GlobalExecuteService 具备多级管理能力。核心代码如下:
@Servicepublic class GlobalExecuteServiceV2 { // 记录当前线程运行级别,默认 0,表示当前线程非该类管理的线程池线程 private static final ThreadLocal<Integer> LEVEL_HOLDER = ThreadLocal.withInitial(()->0); // 一级线程池 private ExecutorService executorServiceLeve1; // 二级线程池 private ExecutorService executorServiceLeve2; // 默认线程池 private ExecutorService defExecutorService; @PostConstruct public void init() { // 省略线程池初始化逻辑 } public <T> Future<T> submit(Callable<T> callable){ // 获取当前线程的运行级别 Integer level = LEVEL_HOLDER.get(); // 根据当前运行级别,计算子任务所使用的线程池 ExecutorService executorService = getNextExecutorServiceByLevel(level); // 为子任务分配运行级别 CallableWrapper<T> callableWrapper = new CallableWrapper<>(level + 1, callable); // 提交任务 return executorService.submit(callableWrapper); } private ExecutorService getNextExecutorServiceByLevel(Integer level) { if (level == 0){ return executorServiceLeve1; } if (level == 1){ return executorServiceLeve2; } return defExecutorService; } class CallableWrapper<T> implements Callable<T>{ private final Integer level; private final Callable<T> callable; CallableWrapper(Integer level, Callable<T> callable) { this.level = level; this.callable = callable; } @Override public T call() throws Exception { try { // 为线程池绑定运行级别 LEVEL_HOLDER.set(level); return callable.call(); }finally { // 清理线程池运行级别 LEVEL_HOLDER.remove(); } } }}
核心设计如下:
为了演示方便,仅定义了 3 级线程池,通常情况下足够业务使用,但需要注意:
- 超过三级提交,仍旧有可以出现死锁的情况,可以通过日志方式及时暴露问题
- 如不放心,可以升级为 “无限极” 设计,及使用 List<ExecutorService> 对线程池进行统一管理,并根据 Level 完成线程池的动态创建
代码仓库:
https://gitee.com/litao851025/learnFromBug
代码地址:
https://gitee.com/litao851025/learnFromBug/tree/master/src/main/java/com/geekhalo/demo/thread/deadlock
本文链接:http://www.28at.com/showinfo-26-70483-0.html故障现场 | 这个死锁出奇的诡异
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com
上一篇: CSS 和 SVG 实现彩色图片阴影
下一篇: 我们一起聊聊React列表渲染与Key