ThreadPoolExecutor 线程池异常消失之刨根问底

昨天,公司一个同事,急急忙忙的跑过来找我,说他的项目,出现了一个非常诡异的BUG,不知道什么情况?

同事:我用五个线程计算学生各个科目的成绩,最后汇总,本地都是正常的,但是一到测试环境就少了一科成绩,也没抛出异常,什么鬼?
油七:任务线程怎么做的?线程异常处理了吗?为啥不打印日志呢?灵魂三连击,哈哈哈(开玩笑的,这不是我的处事风格)
油七:行,咱们先看一下代码...,一顿扫描占卜之后,大致知道啥情况了。
同事:哥,我这程序还有救吗,客户下了死命令,今天解决啊。
油七:没事,小伙子,不要慌,你先把线程池这里 submit 提交改成 execute 试一下
五分钟之后...
同事:卧槽,抛出异常了,我这里计算逻辑有问题,666,这是啥原因啊,为啥我 submit 提交,异常不抛出来啊?
油七:嗯,这个问题...

.

线程池

二、程序模拟

因为同事的代码逻辑比较绕,不便于咱们复现问题,因此我写了一个简单的问题实例,作为本篇文章分析的依据。程序计算用除法代替,除数取到了 0,按道理应该抛出ArithmeticException。

模拟代码

代码如下:

import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class ExceptionMissMain { public static class Task implements Runnable { String name; int a, b; public Task(String name, int a, int b) { this.name = name; this.a = a; this.b = b; } @Override public void run() { double c = a / b; System.out.println("科目:" + name + ", 成绩:" + c); } } public static void main(String[] args) throws InterruptedException { ThreadPoolExecutor es = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS, new SynchronousQueue<>()); for (int i = 0; i < 5; i++) { es.submit(new Task(String.valueOf(i), 100, i)); //es.execute(new Task(String.valueOf(i), 100, i)); Thread.sleep(2000); } } } 结果输出 submit方式 科目:1, 成绩:100.0 科目:2, 成绩:50.0 科目:3, 成绩:33.0 科目:4, 成绩:25.0 缺少一科成绩,程序运行无异常抛出 execute方式 Exception in thread "pool-1-thread-1" java.lang.ArithmeticException: / by zero at com.tiny.juc.boot.pool.ExceptionMissMain$Task.run(ExceptionMissMain.java:30) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) at java.base/java.lang.Thread.run(Thread.java:830) 科目:1, 成绩:100.0 科目:2, 成绩:50.0 科目:3, 成绩:33.0 科目:4, 成绩:25.0 缺少一科成绩,程序运行异常抛出 三、刨根问底

看到上面两种方式提交任务,输出结果的不同,submit方式异常没有了,execute方式抛出了异常,很多人肯定都出现了疑问?

线程池

别纠结了,直接动手刨坟吧,看一看源代码中两个方式究竟是如何实现的,不就真相大白了吗?just do it!我们采用断点调试的方式,一步一步查看程序运行的过程。

源码追击 execute实现

1.首先咱们来看一下execute方法的实现,发现程序正常会进入addWorker方法

线程池

2.咱们来看一下addWorker方法,做了哪些事情?观察下面的代码,我们会发现,addWorker方法先创建了一个Worker对象,并且将传入的Runnable类型的task传入到新建的Worker中,然后再从Worker对象中拿出thread变量,再调用了当前Worker的thread的start方法。疑问:start()方法运行的是什么代码?,Worker对象创建都干了什么事情?Worker对象的thread是怎么创建的?

线程池

线程池

3.带着第二步的疑问,咱们再来一次,这次进到 new Worker 里面,看一下。我们会发现,Worker对象新建的时候,将自己作为目标对象创建了一个线程,并且赋给了Worker中的thread,我们看到Worker类实现了Runnable接口,所以也就是说上一步里面 t.start() 方法,调用的就是目标对象 Worker 自己的 run 方法。

线程池

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zzxxww.html