首先,两个方法都是先进行一个 cas 的操作,把当前 FutureTask 的 status 字段从 NEW 修改为 COMPLETING 。
完成了状态流转的这一步:
注意这里,如果 cas 操作失败了,则不会进行任何操作。
cas 操作失败了,说明什么呢?
说明当前的状态是 CANCELLED 或者 INTERRUPTING 或者INTERRUPTED。
也就是这个任务被取消了或者被中断了。
那还设置结果干啥,没有任何卵用,对不对。
如果 cas 操作成功,接着往下看,可以看到虽然入参不一样了,但是都赋给了 outcome 变量,这个变量,在上一节的 report 方法出现过,还记得吗?能不能呼应上?
接下来就是状态接着往下流转。
set 方法表示正常结束,状态流转到 NORMAL。
setException 方法表示任务出现异常,状态流转到 EXCEPTIONAL。
所以经过 FutureTask 的 run 方法后,如果任务没有被中断或者取消,则会通过 setException 或者 set 方法完成状态的流转和 outcome 参数的设置:
而到底是调用 setException 方法还是 set 方法,取决于标号为 ① 的地方是否会抛出异常。
即取决于任务体是否会抛出异常。
假设 sayHi 方法是这样的,会抛出运行时异常:
而通过 submit 方法提交任务时写法分别如下:
如果是标号为 ① 的写法,则会进入 setException 方法。
如果是标号为 ② 的写法,则会进入 set 方法。
所以,你现在再回去看看这个题目:
当执行方法是 submit 的时候,如果子线程抛出未经捕获的运行时异常,将会被封装到 Future 里面,那么如果子线程捕获了异常,该异常还会封装到 Future 里面吗?是怎么实现的呢?
现在是不是很清晰了。
如果子线程捕获了异常,该异常不会被封装到 Future 里面。是通过 FutureTask 的 run 方法里面的 setException 和 set 方法实现的。在这两个方法里面完成了 FutureTask 里面的 outcome 变量的设置,同时完成了从 NEW 到 NORMAL 或者 EXCEPTIONAL 状态的流转。
线程池拒绝异常
写文章的时候我突然又想到一个问题。
不论是用 submit 还是 execute 方法往线程池里面提交任务,如果由于线程池满了,导致抛出拒绝异常呢?
RejectedExecutionException 异常也是一个 RuntimeException:
那么对于这个异常,如果我们不在子线程捕获,是不是也不会打印呢?
假设你不知道这个问题,你就分析一下,从会和不会中猜一个呗。
我猜是会打印的。
因为假设让我来提供一个这样的功能,由于线程池饱和了而拒绝了新任务的提交,我肯定得给使用方一个提示。告诉他有的任务由于线程池满了而没有提交进去。
不然,使用者自己排查到这个问题后,肯定会说一声:这什么傻逼玩意,把异常给吞了?
来,搞个 Demo 验证一下:
我们定义的这个线程池最大容量是 7 个任务。
在循环体中扔 10 个比较耗时的任务进去。有 3 个任务它处理不了,那么肯定是会触发拒绝策略的。
你觉得这个程序运行后会在控制台打印异常日志吗?会打印几次呢?
看一下运行结果:
抛出了一次异常,执行完成了 7 个任务。
我们并没有捕获异常,打印堆栈信息的相关代码,那么这个异常是谁打印的?
如果你没有捕获异常,JVM 会帮你调用这个方法:
10 个任务,三次异常,完成了 7 个任务。
也不会让 JVM 触发 dispatchUncaughtException 方法了。