zoukankan      html  css  js  c++  java
  • 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 方法。

    线程池

    4.为了验证第三步的解释,我们在 Thread 类中 run 方法与 Worker 类中的 run 方法,分别打上断点,再运行。发现,确实和我们预想的一样,程序先进入了 Thread 类中run 方法,后调用了Worker类中的 run 方法,继而调用了Worker类中 runWorker 方法。

    线程池
    线程池

    5.那么现在,我们再看一下runWorker干了什么事情?我们发现runWorker获取了Worker对象的Runnable task(也就是我们创建的任务),并且调用了我们任务的run 方法。

    线程池
    线程池

    6.OK,我们现在只需要看一下,runWorker task.run()方法调用这里的异常处理,就明白了。我们发现,此处运行有异常捕获,try catch 了Throwable 异常,且向上抛出了,而我们的程序除数取到 0 的异常ArithmeticException,也包括在其中。

    线程池
    线程池

    注释:看到这我们就明白了,前面的程序为什么execute方法会抛出异常了吧,行吧,都散了吧。什么,我才刚看爽,你就叫我走?还有submit呢,为啥不抛异常啊,什么情况还没说呢,别想溜。。。好吧,咱们继续看下 submit的底层实现。

    submit实现

    1.首先咱们来看一下submit方法的实现,发现程序会将我们提交的任务通过newTaskFor方法转换成FutureTask
    2.任务转换成FutureTask后会调用与前面一样的execute 方法

    线程池
    线程池

    3.看到这我们就知道了,也就是说后面还是重复着前面execute执行相同的逻辑,只不过参数变成了FutureTask,那么最后在runWorker方法里面 task.run() 那里,会走FutureTask类的 run 方法,去调用我们定义的任务。
    4.所以我们去FutureTask类中,看一下 run方法的实现。我们发现run 方法中 try catch了异常,并且调用了setException 方法,但是在setException方法中,将异常赋给了outcome,未见其他处理。

    线程池

    5.最后我们看一下FutureTask整个类中outcome 出现的地方,发现在get 方法中通过调用 report 方法返回了 outcome。

    线程池

    6.所以我们在程序那里,通过get方法去接收,看一下出现什么结果?结果同execute方法一样出现了异常。

    Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero
    	at java.base/java.util.concurrent.FutureTask.report(FutureTask.java:122)
    	at java.base/java.util.concurrent.FutureTask.get(FutureTask.java:191)
    	at com.tiny.juc.boot.pool.ExceptionMissMain.main(ExceptionMissMain.java:39)
    

    注释:看到这我们终于明白,submit与execute方法实现上的差异了,以及前文的程序代码为什么submit提交不抛出异常,而execute提交抛出异常了吧。

    四、总结

    1)submit方法,针对异常信息捕获后调用setException 输出到FutureTask 中的outcome;
    2)任务如果是用submit方法提交的,那就用futureTask的get方法去接收;
    3)execute方法会将任务的异常信息,向上抛出;
    4)使用线程池时,需要小心谨慎,做好程序的异常处理,日志记录;

    .

    线程池

  • 相关阅读:
    896. Monotonic Array单调数组
    865. Smallest Subtree with all the Deepest Nodes 有最深节点的最小子树
    489. Robot Room Cleaner扫地机器人
    JavaFX
    《Python CookBook2》 第一章 文本
    《Python CookBook2》 第一章 文本
    《Python CookBook2》 第一章 文本
    《Python CookBook2》 第一章 文本
    《Python CookBook2》 第一章 文本
    《Python CookBook2》 第一章 文本
  • 原文地址:https://www.cnblogs.com/jstarseven/p/14371792.html
Copyright © 2011-2022 走看看