文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

记一次线程池引发的故障 排查下来是谁的锅

2024-12-11 21:32

关注

[[329971]]

 背景

敖丙之前在工作中遇到一个问题,我定义了一个线程池来执行任务,但是程序执行结束后任务没有全部执行完,当时心态就差点崩了。

 

业务场景是这样的:由于统计业务需要,订单信息需要从主库中经过统计业务代码写入统计库(中间需要逻辑处理所以不能走binlog)。

由于代码质量及历史原因,目前的重新统计接口是单线程的,粗略算了算一共有100万条订单信息,每100条的处理大约是10秒,所以理论上处理完全部信息需要28个小时,这还不算因为 mysql 中 limit 分页导致的后期查询时间以及可能出现的内存溢出导致中止统计的情况。

基于上述的原因,以及最重要的一点:统计业务是根据订单所属的中心进行的,各个中心同时统计不会导致脏数据。

所以,我计划使用线程池,为每一个中心分配一条线程去执行统计业务。

业务实现

  1. // 线程工厂,用于为线程池中的每条线程命名 
  2. ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("stats-pool-%d").build(); 
  3.  
  4. // 创建线程池,使用有界阻塞队列防止内存溢出 
  5. ExecutorService statsThreadPool = new ThreadPoolExecutor(5, 10, 
  6.                 0L, TimeUnit.MILLISECONDS, 
  7.                 new LinkedBlockingQueue<>(100), namedThreadFactory); 
  8. // 遍历所有中心,为每一个centerId提交一条任务到线程池 
  9. statsThreadPool.submit(new StatsJob(centerId)); 

在创建完线程池后,为每一个 centerId 提交一条任务到线程池,在我的预想中,由于线程池的核心线程数为5,最多5个中心同时进行统计业务,将大大缩短100万条数据的总统计时间,于是万分兴奋的我开始执行重新统计业务了。

问题

在跑了很久之后,当我查看统计进度时,我发现了一个十分诡异的问题(如下图)。

蓝框标出的这条线程是 WAIT 状态,表明这条线程是空闲状态,但是从日志中我看到这条线程并没有完成它的任务,因为这个中心的数据有10万条,但是日志显示它只跑到了一半,之后就再无关于此中心的日志了。

 

 

这是什么原因?

我当场就想到了三歪,肯定是三歪今天早上上班左脚先迈进公司的,导致代码水土不服,一定是这样,我去找他去。

 

调试及原因

咳咳三歪是开玩笑的,我们还是需要找到真实原因。

可以想到的是,这条线程因为某些原因被阻塞了,并且没有继续进行下去,但是日志又没有任何异常信息…

可能有经验的工程师已经知道了原因…

由于个人水平的线程,暂时没有找到原因的我只能放弃使用线程池,乖乖用单线程跑…

幸运的是,单线程跑的任务竟然抛错了(为什么要说幸运?),于是马上想到,之前那条 WAIT 状态的线程可能是因为同样的抛错所以被中断了,导致任务没有继续进行下去。

为什么说幸运?因为如果单线程的任务没有抛错的话,我可能很久都想不到是这个原因。

 

深入探究线程池的异常处理

工作上的问题到这里就找到原因了,之后的解决过程也十分简单,这里就不提了。

但是疑问又来了,为什么使用线程池的时候,线程因异常被中断却没有抛出任何信息呢?还有平时如果是在 main 函数里面的异常也会被抛出来,而不是像线程池这样被吞掉。

如果子线程抛出了异常,线程池会如何进行处理呢?

我提交任务到线程池的方式是: threadPoolExecutor.submit(Runnbale task); ,后面了解到使用 execute() 方式提交任务会把异常日志给打出来,这里研究一下为什么使用 submit 提交任务,在任务中的异常会被“吞掉”。

对于 submit() 形式提交的任务,我们直接看源码:

  1. public Future submit(Runnable task) { 
  2.     if (task == null) throw new NullPointerException(); 
  3.     // 被包装成 RunnableFuture 对象,然后准备添加到工作队列 
  4.     RunnableFuture ftask = newTaskFor(task, null); 
  5.     execute(ftask); 
  6.     return ftask; 

它会被线程池包装成 RunnableFuture 对象,而最终它其实是一个 FutureTask 对象,在被添加到线程池的工作队列,然后调用 start() 方法后, FutureTask 对象的 run() 方法开始运行,即本任务开始执行。

  1. public void run() { 
  2.     if (state != NEW || !UNSAFE.compareAndSwapObject(this,runnerOffset,null, Thread.currentThread())) 
  3.         return
  4.     try { 
  5.         Callable c = callable; 
  6.         if (c != null && state == NEW) { 
  7.             V result; 
  8.             boolean ran; 
  9.             try { 
  10.                 result = c.call(); 
  11.                 ran = true
  12.             } catch (Throwable ex) { 
  13.                 // 捕获子任务中的异常 
  14.                 result = null
  15.                 ran = false
  16.                 setException(ex); 
  17.             } 
  18.             if (ran) 
  19.                 set(result); 
  20.         } 
  21.     } finally { 
  22.         runner = null
  23.         int s = state; 
  24.         if (s >= INTERRUPTING) 
  25.             handlePossibleCancellationInterrupt(s); 
  26.     } 

在 FutureTask 对象的 run() 方法中,该任务抛出的异常被捕获,然后在setException(ex); 方法中,抛出的异常会被放到 outcome 对象中,这个对象就是 submit() 方法会返回的 FutureTask 对象执行 get() 方法得到的结果。

但是在线程池中,并没有获取执行子线程的结果,所以异常也就没有被抛出来,即被“吞掉”了。

这就是线程池的 submit() 方法提交任务没有异常抛出的原因。

线程池自定义异常处理方法

在定义 ThreadFactory 的时候调用

setUncaughtExceptionHandler方法,自定义异常处理方法。例如:

  1. ThreadFactory namedThreadFactory = new ThreadFactoryBuilder() 
  2.                 .setNameFormat("judge-pool-%d"
  3.                 .setUncaughtExceptionHandler((thread, throwable)-> logger.error("ThreadPool {} got exception", thread,throwable)) 
  4.                 .build(); 

这样,对于线程池中每条线程抛出的异常都会打下 error 日志,就不会看不到了。

后续

在修复了单个线程任务的异常之后,我继续使用线程池进行重新统计业务,终于跑完了,也终于完成了这个任务。

事后我也叫三歪以后进公司一定要先迈出右脚进来,不然对写代码的风水影响很大。

 

小结:丙这个事故也给大家一个警示,使用线程池时需要注意,子线程的异常,如果没有被捕获就会丢失,可能会导致后期根据日志调试时无法找到原因。

 

来源:三太子敖丙内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯