当前位置:首页 > 科技  > 软件

故障现场 | 这个死锁出奇的诡异

来源: 责编: 时间:2024-02-01 12:52:21 311观看
导读1. 问题&分析线程池用多了总会出现些诡异问题,特别是当任务间的关系比较复杂时,经常会出现让你想象不到问题,比如这次出现的这个问题。1.1. 案例突然间,系统出现大量报警,具体信息如下:图片从抛出的异常可知,提交量较大导致

1. 问题&分析

线程池用多了总会出现些诡异问题,特别是当任务间的关系比较复杂时,经常会出现让你想象不到问题,比如这次出现的这个问题。38H28资讯网——每日最新资讯28at.com

1.1. 案例

突然间,系统出现大量报警,具体信息如下:38H28资讯网——每日最新资讯28at.com

图片图片38H28资讯网——每日最新资讯28at.com

从抛出的异常可知,提交量较大导致线程池资源被耗尽,从而触发了线程池的拒绝策略,直接抛出了 RejectedExecutionException。38H28资讯网——每日最新资讯28at.com

开始的时候,小艾认为等高峰流量过去后,系统便能恢复正常。可出乎意料的是,系统一直没有恢复,那么流量已经将至个位数,请求也是 100% 失败,同时该节点的大量后台任务都出现异常。没有办法,为了快速止损,不得已对异常节点进行重启,系统随之恢复正常,日志输入如下:38H28资讯网——每日最新资讯28at.com

图片图片38H28资讯网——每日最新资讯28at.com

其他的后台任务也恢复正常。38H28资讯网——每日最新资讯28at.com

惊魂初定的小艾找到出问题的代码如下:38H28资讯网——每日最新资讯28at.com

@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();    }}

代码的逻辑非常简单,核心流程如下图所示:38H28资讯网——每日最新资讯28at.com

图片图片38H28资讯网——每日最新资讯28at.com

核心流程为:38H28资讯网——每日最新资讯28at.com

  1. 提交异步任务,并行获取 A 和 B
  2. 线程同步处理一下耗时操作
  3. 获取 A 和 B 结果后,构建新对象 C
  4. 将 C 保存到数据库

逻辑非常简单,唯一的复杂点在于:==多处任务提交使用了统一线程池。==38H28资讯网——每日最新资讯28at.com

【背景】考虑到线程池是系统中最宝贵的资源,公司“大牛”封装了一个全局的 GlobalExecuteService 服务,并制定规范要求所有异步任务统一使用 GlobalExecuteService 来完成。如果需要构建自己的线程池,需要向他提交审批,只有在审批后才能创建新的线程池。38H28资讯网——每日最新资讯28at.com

1.2. 问题分析

线程池处于什么状态?为什么所有异步任务都无法提交?38H28资讯网——每日最新资讯28at.com

这是一个比较烧脑的问题,单盘逻辑没有什么头绪,没有办法只能将现场 dump 下来进行分析。38H28资讯网——每日最新资讯28at.com

第一个问题:线程池线程都处于什么状态,线程栈信息如下:38H28资讯网——每日最新资讯28at.com

图片图片38H28资讯网——每日最新资讯28at.com

从日志中可知:38H28资讯网——每日最新资讯28at.com

  1. 线程池中的线程全部处于 WAITING,也就是等待状态;
  2. 展开 Thread-1 栈信息,发现线程再调用 future.get 操作时出现阻塞
  3. 实际等待对象为 FutrueTask#10

接下来,需要进一步确认 FutureTask#10 具体处于什么状态,从内存堆中找到 FutureTask#10 对象,详细信息如下:38H28资讯网——每日最新资讯28at.com

图片图片38H28资讯网——每日最新资讯28at.com

从日志中可以看出:38H28资讯网——每日最新资讯28at.com

  1. FutureTask#10 是 LinkedBlockingQueue 的 Node 节点持有,也就是 FutureTask#10 处于等待队列中
  2. 该阻塞队列属于 GlobalExecuteService 所有

排查到这里,真相浮出水面:GlobalExecuteService 中的线程,正在等待 GlobalExecuteService 阻塞队列的任务完成。38H28资讯网——每日最新资讯28at.com

具体如下图所示:38H28资讯网——每日最新资讯28at.com

图片图片38H28资讯网——每日最新资讯28at.com

线程池中的所有工作线程都在等待阻塞队列的任务完成,由于没有可用的工作线程,阻塞队列中的任务永远都不会被执行。38H28资讯网——每日最新资讯28at.com

这就是典型的死锁!!!38H28资讯网——每日最新资讯28at.com

2. 解决方案

费了老大劲终于定位问题,解决思路也就变的明了:不要向自己运行的线程池提交任务。38H28资讯网——每日最新资讯28at.com

图解如下:38H28资讯网——每日最新资讯28at.com

图片图片38H28资讯网——每日最新资讯28at.com

线程池不会向自己提交任务,而是将任务提交到其他线程池。38H28资讯网——每日最新资讯28at.com

2.1. 手工拆分线程池

问题修复变的简单,我们需要:38H28资讯网——每日最新资讯28at.com

  1. 先建一个子线程池服务
  2. 父线程池向子线程池提交任务

具体代码如下:38H28资讯网——每日最新资讯28at.com

@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;    }}

2.2. 多级任务管理

手工拆分线程池确实能解决这个场景的问题,但由于 GlobalExecuteService 服务已经使用很长时间,任务间的关系错综复杂,很难一次性排查并修复所有问题,同时随着逻辑的变化未来仍旧会出现类似的问题。38H28资讯网——每日最新资讯28at.com

那最佳方案是什么?38H28资讯网——每日最新资讯28at.com

让 GlobalExecuteService 具备多级管理能力。核心代码如下:38H28资讯网——每日最新资讯28at.com

@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();            }        }    }}

核心设计如下:38H28资讯网——每日最新资讯28at.com

  1. 使用 ThreadLocal<Integer> LEVEL_HOLDER 记录当前线程运行的级别,默认为 0 表示任务未在线程池中运行
  2. 提交任务时,通过当前运行级别计算下一级别的线程池
  • 当前级别为0,返回 Level1 线程池
  • 当前级别为1,返回 Level2 线程池
  • 其他基本,返回 默认 线程池
  1. 通过 CallableWrapper 自动将任务运行的 Level 绑定到当前线程上下文
  2. 任务执行前,使用 LEVEL_HOLDER.set(level) 完成运行 level 的设置
  3. 任务执行后,使用 LEVEL_HOLDER.remove() 完成运行 level 的清理

为了演示方便,仅定义了 3 级线程池,通常情况下足够业务使用,但需要注意:38H28资讯网——每日最新资讯28at.com

  • 超过三级提交,仍旧有可以出现死锁的情况,可以通过日志方式及时暴露问题
  • 如不放心,可以升级为 “无限极” 设计,及使用 List<ExecutorService> 对线程池进行统一管理,并根据 Level 完成线程池的动态创建

3. 示例&源码

代码仓库:38H28资讯网——每日最新资讯28at.com

https://gitee.com/litao851025/learnFromBug38H28资讯网——每日最新资讯28at.com

代码地址:38H28资讯网——每日最新资讯28at.com

https://gitee.com/litao851025/learnFromBug/tree/master/src/main/java/com/geekhalo/demo/thread/deadlock38H28资讯网——每日最新资讯28at.com

本文链接:http://www.28at.com/showinfo-26-70483-0.html故障现场 | 这个死锁出奇的诡异

声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com

上一篇: CSS 和 SVG 实现彩色图片阴影

下一篇: 我们一起聊聊React列表渲染与Key

标签:
  • 热门焦点
  • Mate60手机壳曝光 致敬自己的经典设计

    8月3日消息,今天下午博主数码闲聊站带来了华为Mate60的第三方手机壳图,可以让我们在真机发布之前看看这款华为全新旗舰的大致轮廓。从曝光的图片看,Mate 60背后摄像头面积依然
  • 小米官宣:2023年上半年出货量中国第一!

    今日早间,小米电视官方微博带来消息,称2023年小米电视上半年出货量达到了中国第一,同时还表示小米电视的巨屏风暴即将开始。“公布一个好消息2023年#小米电视上半年出货量中国
  • 小米降噪蓝牙耳机Necklace分享:听一首歌 读懂一个故事

    在今天下午的小米Civi 2新品发布会上,小米还带来了一款新的降噪蓝牙耳机Necklace,我们也在发布结束的第一时间给大家带来这款耳机的简单分享。现在大家能见到最多的蓝牙耳机
  • 三言两语说透设计模式的艺术-简单工厂模式

    一、写在前面工厂模式是最常见的一种创建型设计模式,通常说的工厂模式指的是工厂方法模式,是使用频率最高的工厂模式。简单工厂模式又称为静态工厂方法模式,不属于GoF 23种设计
  • 量化指标是与非:挽救被量化指标扼杀的技术团队

    作者 | 刘新翠整理 | 徐杰承本文整理自快狗打车技术总监刘新翠在WOT2023大会上的主题分享,更多精彩内容及现场PPT,请关注51CTO技术栈公众号,发消息【WOT2023PPT】即可直接领取
  • 从零到英雄:高并发与性能优化的神奇之旅

    作者 | 波哥审校 | 重楼作为公司的架构师或者程序员,你是否曾经为公司的系统在面对高并发和性能瓶颈时感到手足无措或者焦头烂额呢?笔者在出道那会为此是吃尽了苦头的,不过也得
  • 一个注解实现接口幂等,这样才优雅!

    场景码猿慢病云管理系统中其实高并发的场景不是很多,没有必要每个接口都去考虑并发高的场景,比如添加住院患者的这个接口,具体的业务代码就不贴了,业务伪代码如下:图片上述代码有
  • 2天涨粉255万,又一赛道在抖音爆火

    来源:运营研究社作者 | 张知白编辑 | 杨佩汶设计 | 晏谈梦洁这个暑期,旅游赛道彻底火了:有的「地方」火了&mdash;&mdash;贵州村超旅游收入 1 个月超过 12 亿;有的「博主」火了&m
  • 小米公益基金会捐赠2500万元驰援北京、河北暴雨救灾

    8月2日消息,今日小米科技创始人雷军在其微博上发布消息称,小米公益基金会宣布捐赠2500万元驰援北京、河北暴雨救灾。携手抗灾,京冀安康!以下为公告原文
Top