一、情景复现

昨天,公司一个同事,急急忙忙的跑过来找我,说他的项目,出现了一个非常诡异的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)使用线程池时,需要小心谨慎,做好程序的异常处理,日志记录;

.

ThreadPoolExecutor 线程池异常消失之刨根问底的更多相关文章

  1. ThreadPoolExecutor 线程池的源码解析

    1.背景介绍 上一篇从整体上介绍了Executor接口,从上一篇我们知道了Executor框架的最顶层实现是ThreadPoolExecutor类,Executors工厂类中提供的newSchedul ...

  2. j.u.c系列(01) ---初探ThreadPoolExecutor线程池

    写在前面 之前探索tomcat7启动的过程中,使用了线程池(ThreadPoolExecutor)的技术 public void createExecutor() { internalExecutor ...

  3. Java并发——ThreadPoolExecutor线程池解析及Executor创建线程常见四种方式

    前言: 在刚学Java并发的时候基本上第一个demo都会写new Thread来创建线程.但是随着学的深入之后发现基本上都是使用线程池来直接获取线程.那么为什么会有这样的情况发生呢? new Thre ...

  4. ThreadPoolExecutor 线程池

    TestThreadPoolExecutorMain package core.test.threadpool; import java.util.concurrent.ArrayBlockingQu ...

  5. 十、自定义ThreadPoolExecutor线程池

    自定义ThreadPoolExecutor线程池 自定义线程池需要遵循的规则 [1]线程池大小的设置 1.计算密集型: 顾名思义就是应用需要非常多的CPU计算资源,在多核CPU时代,我们要让每一个CP ...

  6. 手写线程池,对照学习ThreadPoolExecutor线程池实现原理!

    作者:小傅哥 博客:https://bugstack.cn Github:https://github.com/fuzhengwei/CodeGuide/wiki 沉淀.分享.成长,让自己和他人都能有 ...

  7. 源码剖析ThreadPoolExecutor线程池及阻塞队列

    本文章对ThreadPoolExecutor线程池的底层源码进行分析,线程池如何起到了线程复用.又是如何进行维护我们的线程任务的呢?我们直接进入正题: 首先我们看一下ThreadPoolExecuto ...

  8. 手写一个线程池,带你学习ThreadPoolExecutor线程池实现原理

    摘要:从手写线程池开始,逐步的分析这些代码在Java的线程池中是如何实现的. 本文分享自华为云社区<手写线程池,对照学习ThreadPoolExecutor线程池实现原理!>,作者:小傅哥 ...

  9. 13.ThreadPoolExecutor线程池之submit方法

    jdk1.7.0_79  在上一篇<ThreadPoolExecutor线程池原理及其execute方法>中提到了线程池ThreadPoolExecutor的原理以及它的execute方法 ...

随机推荐

  1. linux操作系统可以ping通ssh连接长时间无响应

    一.问题描述 某集群数据节点服务器频繁无法连接,服务器间出现可ping通但ssh无法连接的情况,使用带外地址登录后远程控制也无法显示正常界面,重启后会短暂恢复. 二.排查问题 重启服务器后检查服务器S ...

  2. MongoDB Sharding(二) -- 搭建分片集群

    在上一篇文章中,我们基本了解了分片的概念,本文将着手实践,进行分片集群的搭建 首先我们再来了解一下分片集群的架构,分片集群由三部分构成: mongos:查询路由,在客户端程序和分片之间提供接口.本次实 ...

  3. 【Oracle】查看表空间是否为自动扩展

    查看指定的表空间是否为自动扩展 SQL>   select file_name,autoextensible,increment_by from dba_data_files where tab ...

  4. kubernets之节点和网络的安全保障策略

    一  在pod中使用宿主节点的命名空间 1.1  在pod中使用宿主的网络命名空间 [root@node01 Chapter13]# cat pod-with-host-network.yml api ...

  5. Sqli - Labs 靶场笔记(一)

    Less - 1: 页面: URL: http://127.0.0.1/sqli-labs-master/Less-1/ 测试: 1.回显正常,说明不是数字型注入, http://127.0.0.1/ ...

  6. List使用Stream流进行集合Collection的各种运算汇总:对BigDecimal求和,某个字段的和、最大值、最小值、平均值,字段去重,过滤等

    写Java接口的朋友都知道,Java 8的更新,经常会用到过滤 list<Object> 里的数据,本文就对List使用Stream流进行集合Collection的各种运算做一个汇总! 优 ...

  7. [Usaco2007 Jan]Balanced Lineup 飞盘比赛

    题目描述 每天,农夫 John 的N(1 <= N <= 50,000)头牛总是按同一序列排队. 有一天, John 决定让一些牛们玩一场飞盘比赛. 他准备找一群在对列中为置连续的牛来进行 ...

  8. floating point

    记录浮点数的单精度和双精度(IEEE754) 1.单精度(float) ​ 1.定义:单精度占4字节/32位,其中1号位符号位,其次是8位阶码/指数(阶符+阶数),23位尾数(小数). 2.双精度(d ...

  9. Django中多表关联的展示问题:

    增加一个知识点,当表中有多对多关联时,在前端展示的时候是一个列表,所以在展示的时候需要这样做: 表结构: class ProjectEnv(models.Model): project = model ...

  10. 处理 K8S Orphaned pod found - but volume paths are still present on disk 孤儿pod

    问题概述 查看kubelet或/var/log/messages日志一直包错,发现是孤儿pod,是由于其pod被删除后存储路径还保存在磁盘. 报错如下 [root@node5 ~]# journalc ...