前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >继续说那个死了的线程的事儿

继续说那个死了的线程的事儿

作者头像
Java阿呆
发布2021-04-30 16:06:45
6870
发布2021-04-30 16:06:45
举报
文章被收录于专栏:Java阿呆Java阿呆

嗨,朋友,好久不见,我是阿呆。

今天接着上一篇继续来说说那个死了的线程的事,同时补充一下前文的一些小漏洞,和一些扩展。

上一篇传送门:有人关心过线程池里死掉的线程吗?不然这篇可能看不明白。

前文提出了三个猜测和对应的答案,并且证明了第一条,那么今天就把剩下的两条先证明一下:

  1. 抛出堆栈异常? ---这句话对了一半!
  2. 不影响其他线程任务? ---这句话全对!
  3. 这个线程会被放回线程池?---这句话全错!

首先,为什么说线程池里死掉的线程不会影响其他的线程任务呢?还是先写段代码看看效果:

很明显,程序运行结果是不会骗人的,俗话说,程序很单纯,复杂的是人。一个抛异常,四个正常执行,没有任何问题,这段代码也就印证了第二个结论,证明完毕。

但是这里有一个小细节,本来应该是1、2、4、5执行成功,3报错,可是却跑出来个6,而4却不见了,怎么回事?

这个问题会随着第三个结论的得证迎刃而解,come on!

第三个问题直接看源码,在ThreadPoolExecutor的runWorker()方法里:

当一个线程异常时,会到finally块的processWorkerExit方法,答案就在这里,进去看看:

到这里,结论三的答案已经很明显了,就是线程池会把发生异常的线程移除掉,并且调用addWorker新建一个线程放到线程池。

那么问题来了,刚才执行结果中的线程4去哪里了?

答案就在addWorker里,进去看看:

到这里,找到了线程id自增的线索,但是好像还是没有找到为什么4不见了,别着急,慢慢分析一波:

因为多线程运行的结果变幻莫测,所以这次分析只针对上面的运行结果,但是道理是一样的。

首先,任务1、2调用addWorker,新建了线程1、2,没有任何问题,到任务3这,首先调用addWorker,新建了线程3,但是线程3异常了,通过上面的源码可以看到,异常后会新建一个线程,并且再调一次addWorker,注意,这个时候,任务4也准备调addWorker,这两次调用在正常情况下都会创建线程4,所以肯定会有个谁先谁后的问题,如果任务4先调了addWorker,那么肯定就是线程4去执行任务4,线程4就不会丢,但是如果是异常的线程3先调addWorker,那么线程ID自增到4,这时候任务4再去调addWorker,就产生了线程5来执行任务4,那么线程4就丢了。

明白了吗,反正我是晕了。


好了,到这里,剩下的两个结论已经证明完了,接下来说一下上一篇文章中的一些小细节。

第一个细节:submit()方法返回Future,是个接口,这个接口有众多的实现类,为什么直接就看FutureTask?先知吗?当然不是!当然是源码中的答案啦:

submit方法调用后,会将task进行一层包装,包装成什么呢,看看:

看到没有!一个大大的FutureTask!

第二个细节:如果子线程的那个异常,在启动线程的时候被捕获了,那么调用get()方法还会打印堆栈吗?用脚后跟都能想到,不会打印,但是总得有个说法吧。那就来分析一波嘛。

首先,这个问题讨论的是,线程的异常是否在子线程中捕获,对调用get()方法是否打印异常的影响,那就从get()方法入手,看看子线程对异常的捕获与否到底影响什么,思路清晰,目标明确,开干!

看什么来着?嗷对,FutureTask的get()方法!为什么是FutureTask?往上看一看兄弟!

诺~get()方法,很简洁,但是可不要小瞧了这几行代码,还是有点东西的,比如说:state是哪里冒出来的,是什么意思?分支条件的判断可能能猜到,是判断状态是否是进行中(完成中我们翻译成进行中),那么这个状态又是哪里冒出来的?awaitDone方法和report方法里又干了什么?

别急,要回答这些问题,得从FutureTask的根源看起。

看下FutureTask的顶部:

好家伙,主角们一下都出来了,这里有一个volatile的state变量,表示线程的当前状态,和几个用数字代表的状态值,并且在注释里很清楚的描述了几种状态之间所有可能的流转关系,是不是非常的一目了然?

这个时候,再回头分析一波,看下get()方法:

如果状态小于等于COMPLETING,则进入awaitDone方法等待,否则进入report方法,那么小于等于COMPLETING的状态有什么呢?只有NEW和COMPLETING,也就是新建和进行中,总之就是没有完成,就进入等待,不是我们讨论的重点,所以继续往下看。

要想进入report方法,状态必须要大于COMPLETING,也就是说碧玺得是这些其中一个:

好,然后进到report方法:

这个方法注释写的很清楚:对于已完成的任务,返回结果或抛出异常。

代码也很简单,如果是正常状态,就返回结果,如果是大于等于取消状态,就抛出一个取消异常。其他的情况,抛出一个执行异常,那其他的情况是什么呢?不就只剩了下EXCEPTIONAL嘛。

注意这里这个outcome变量,它是关键!记住了!

现在知道了异常是哪里抛出来的,那么再看,这里是否抛出异常和子线程是否捕获异常有什么关联。

再走一遍submit流程:

创建一个FutureTask:

看到没有,这里把线程状态初始化为NEW。

然后就该到了run方法:

注意框起来的三处,c.call()就是调用我们的执行方法,也就是sayHi(),那么!!!!答案就来了!

如果对sayHi()方法捕获了异常,那就不会被catch到,就走了第三个框;

如果对sayHi()方法的异常没有捕获,那么就会被catch到走第二个框。

看看这二者的区别是什么,答案马上就来了,激不激动!

这俩方法都是在一块定义的,你说说:

这区别一目了然,首先都进行了同一个操作:通过CAS,把线程状态改成进行中,这也是前提,如果这一步没成功,说明线程状态不是NEW,不是NEW是啥?再看一眼状态流转:

那就只能是CANCELLED或者INTERRUPTING再或者INTERRUTED,依然是取消或者中断,那还处理什么结果呢,没有结果的呀!

所以接着往下看,重点来了,这两个方法给outcome变量赋了不同的值,没有异常的情况下是把执行结果给了outcome,而有异常的情况是直接把异常给了outcome,这个outcome变量还记得吗?report方法里就是判断的outcome变量决定的不同操作,得呼应上啊!

接下来就是状态的继续流转,没啥可说的。

到这里,再回头看看最开始的那个问题:为啥子线程里捕获了异常再调用get()方法不会抛异常?是不是很清晰了。

答案就是:

如果子线程捕获了异常,该异常不会被封装到 Future 里面。是通过 FutureTask 的 run 方法里面的 setException 和 set 方法实现的。在这两个方法里面完成了 FutureTask 里面的 outcome 变量的设置,同时完成了从 NEW 到 NORMAL 或者 EXCEPTIONAL 状态的流转。


到这里,基本就已经把该说的说完了,但是,你以为这就结束了?还没有呢,别走啊!还有更终极的答案呢!

再考虑一个问题,既然用submit方式提交的线程发生异常但并未捕获时,不会打印异常信息,那么如果线程池满了,抛出的拒绝异常RejectedExecutionException,会打印堆栈信息吗?这个异常也是一个RuntimeException,是不是觉得也不会打印,看看就知道了。

这段代码肯定会触发线程池拒绝异常,那么异常信息会打印吗?打印的话又会打印几次呢?运行一下见分晓:

可以看到,有七条正常执行的日志,一条异常信息,这太不正常了。

我们代码中没有任何捕获异常并打印日志的代码,这异常日志是哪里来的?而且执行方式是submit,不是说没有捕获的异常在调用get()方法的时候才会打印吗?而且即使打印的话,为什么只有一条,应该是三条才对啊?

带着这些疑问,继续探索终极答案!

其实在Thread类里有这么一个方法:

注释里写着:

分发一个未捕获的异常到处理器,本方法只能由JVM自动调用

好家伙,原来是这玩意搞的鬼,所以在没有捕获异常时候,jvm会自动触发一次这个方法,打印一次堆栈,如果我们捕获了异常:

那么就会正常打印三条堆栈信息:


所以现在让我们总结一下终极答案!

如果以submit方式提交一个会发生异常的任务,无论子线程捕不捕获异常,都不会触发dispatchUncaughtException()方法,因为无论子线程捕不捕获异常,源码里都给捕获了,调用get()方法才能拿到这个异常。

如果以execute方式提交一个会发生异常的任务,而子线程又没有捕获异常,那么就会触发dispatchUncaughtException()方法,因为在execute的源码里,对虽然对异常进行了捕获,但是又抛出去了,导致触发dispatchUncaughtException()方法,所以execute会看到异常信息。

如果提交任务的时候导致线程池饱和,触发了拒绝异常,而子线程又没有捕获异常,那么无论是submit方式还是execute方式,都会触发dispatchUncaughtException()方法,而且只触发一次。


这下!是不是清晰了!不清晰?再看几遍!

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-04-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Coder阿呆 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档