专栏首页why技术关于多线程中抛异常的这个面试题我再说最后一次!

关于多线程中抛异常的这个面试题我再说最后一次!

这里why的第 66 篇原创文章

一道面试题

我一年前写过这篇文章《有的线程它死了,于是它变成一道面试题》,这是早期作品,遣词造句,排版行文都有一点稚嫩,但是承蒙厚爱,还是有很多人看过。

甚至已经进入了某网红公司的面试题库里面。

本文相当于是对上面这篇文章的一个补充。

现在先回顾一下这篇文章抛出的问题和问题的答案:

一个线程池中的线程异常了,那么线程池会怎么处理这个线程?

这个题是我遇到的一个真实的面试题,当时并没有回答的很好。然后通过上面的文章,我在源码中寻找到了答案。

先给大家看两个案例。

sayHi 方法是会抛出运行时异常的。

当执行方式是 execute 方法时,在控制台会打印堆栈异常:

当执行方式是 submit 方法时,在控制台不会打印堆栈异常:

那么怎么获取这个 submit 方法提交时的异常信息呢?

得调用返回值 future 的 get 方法:

具体原因,我在之前的文章里面详细分析过,就不赘述了,直接看结论:

然后一个读者找我聊天,说为什么他这样写,通过 future.get 方法没有抛出异常呢,和我文章里面说的不一样呢?

我说:那肯定是你操作不对,你把代码发给我看看。

然后我收到了一份这样的代码:

public class ExecutorsTest {

    public static void main(String[] args) {
        ThreadPoolExecutor executorService = new ThreadPoolExecutor(2, 2,
                30, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10));
        Future future = executorService.submit(() -> {
            try {
                sayHi("submit");
            } catch (Exception e) {
                System.out.println("sayHi Exception");
                e.printStackTrace();
            }
        });

        try {
            future.get();
        } catch (Exception e) {
            System.out.println("future.get Exception");
            e.printStackTrace();
        }
    }

    private static void sayHi(String name) throws RuntimeException {
        String printStr = "【thread-name:" + Thread.currentThread().getName() + ",执行方式:" + name + "】";
        System.out.println(printStr);
        throw new RuntimeException(printStr + ",我异常啦!哈哈哈!");
    }
}

这个程序的输出结果是这样的:

我寻思这没毛病呀,这不是很正常吗?不就是应该这样输出吗?

那个哥们说:和你说的不一样啊,你说的是调用 future.get 方法的时候会抛出异常的?我这里并没有输出“future.get Exception”,说明 future.get 方法没有抛出异常。

我回答到:你这不是把会抛出运行时异常的 sayHi 方法用 try/catch 代码块包裹起来了吗?异常在子线程里面就处理完了,也就不会封装到 Future 里面去了。你把 try/catch 代码块去掉,异常就会封装到 Future 里面了。

过了一小会,他应该是实验完了,又找过来了。

他说:牛逼呀,确实是这样的。那你的这个面试题是有问题的啊,描述不清楚,正确的描述应该是一个线程池中的线程抛出了未经捕获的运行时异常,那么线程池会怎么处理这个线程?

看到他的这个回复的时候,我竟然鼓起掌来,这届读者真是太严格了!但是他说的确实是没有错,严谨点好。

他还追问到:怎么实现的呢?为什么当 submit 方法提交任务的时候,子线程捕获了异常,future.get 方法就不抛出异常了呢?

其实听到这个问题的时候都把我干懵了。

这问法,难道你是想再抛一次异常出来?

其实大家按照正常的思维去想,都能知道如果子线程捕获了一次,future.get 方法就不应该抛出异常了。

所以,现在的问题是,这个小小的功能,在线程池里面是怎么实现的?

现在的面试题在原来的基础上再加一层:

好,你说当执行方法是 submit 的时候,如果子线程抛出未经捕获的运行时异常,将会被封装到 Future 里面?那么如果子线程捕获了异常,该异常还会封装到 Future 里面吗?是怎么实现的呢

寻找答案-FUTURE

来,一起去源码里面寻找答案。

现在是用 submit 的方式往线程池里面提交任务,而执行的这个任务会抛出运行时异常。

对于抛出的这个异常,我们分为两种情况:

  • 子线程中捕获了异常,则调用返回的 future 的 get 方法,不会抛出异常。
  • 子线程中没有捕获异常,则调用返回的 future 的 get 方法,会抛出异常。

两种情况都和 future.get 方法有关,那我们就从这个方法的源码入手。

这个 Future 是一个接口:

而这个接口有非常多的实现类。我们找哪个实现类呢?

就是下面这个实现类:

java.util.concurrent.FutureTask

至于是怎么找到它的,你慢慢往后看就知道了。

先看看 FutureTask 的 get 方法:

get 方法的逻辑很简单,首先判断当前状态是否已完成,如果不是,则进入等待,如果是,则进入 report 方法。

一进 get 方法,我们就看到了 state 这个东西,这是 FutureTask 里面一个非常重要的东西:

在 FutureTask 里面,一共有 7 种状态。这 7 种状态之间的流转关系已经在注释里面写清楚了。

状态之间只会按照这四个流程去流转。

所以,一目了然,一个任务的终态有四种:NORMAL、EXCEPTIONAL、CANCELLED、INTERRUPTED。

而我们主要关心 NORMAL、EXCEPTIONAL。

所以再回头看看 get 方法:

如果当前状态是小于 COMPLEING 的。

也就是当前状态只能是 NEW 或者 COMPLEING,总之就是任务还没有完成。所以进入 awaitDone 方法。这个方法不是本文关心的地方,接着往下看。

程序能往下走,说明当前的状态肯定是下面圈起来的状态中的某一个:

记住这几种状态,然后看这个 report 方法:

这个方法是干啥的?

注解说的很清楚了:对于已经完成了的 task,返回其结果或者抛出异常。

这里面的逻辑就很简单了,把 outcome 变量赋值给 x 。

然后判断当前状态,如果是 NORMAL,即 2,说明正常完成,直接返回 x。

如果是大于等于 CANCELLED,即大于等于 4 ,即这几种状态,就抛出 CancellationException。

剩下的情况就抛出 ExecutionException。

而这个“剩下的情况”是什么情况?

不就只剩下一个 EXCEPTIONAL 的情况了。

所以,经过前面的描述,我们可以总结一下。

当 FutureTask 的 status 为 NORMAL 时正常返回结果,当 status 为 EXCEPTIONAL 时抛出异常。

而当终态为 NORMAL 或者 EXCEPTIONAL 时,按照注释描述,状态的流程只能是这样的:

那么到底是不是这样的呢?

这就需要我们去线程池里面验证一下了。

寻找答案-线程池

先回答上一节的一个问题:我怎么知道是看 Future 这个接口的 FutureTask 这个实现类的:

submit 方法提交的时候把任务包裹了一层,就是用 FutureTask 包裹的:

可以看到,FutureTask 的构造方法里面默认了状态为 NEW。

然后直接在 runWorker 方法的 task.run 方法处打上断点:

这个 task 是一个 FutureTask,所以 run 方法其实是 FutureTask 的 run 方法。

跟着断点进去之后,就是 FutureTask 的 run 方法:

答案都藏在这个方法里面。

java.util.concurrent.FutureTask#run

标号为 ① 的地方是执行我们的任务,call 的就是示例代码里面的 sayHi 方法。

如果提交的任务( sayHi 方法)抛出的运行时异常没有被捕获,则会在标号为 ② 的这个 catch 里面被捕获。然后执行标号为 ② 的这个代码。

如果提交的任务( sayHi 方法)捕获了运行时异常,则会进入标号为 ③ 的这个逻辑里面。

我们分别看一下标号为 ② 和 ③ 的逻辑:

首先,两个方法都是先进行一个 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 方法了。

而这个异常日志的打印和哪种方式提交任务没有关系,不论哪种,只要你没有捕获异常,则都会触发 dispatchUncaughtException 方法。

终极答案

上面说这个例子,其实我就是想引出终极答案。

终极答案就是:dispatchUncaughtException 方法。

为什么这样说呢?

我们现在把情况分为三种。

第一种:submit 方法提交一个会抛出运行时异常的任务,捕不捕获异常都可以。

第二种:execute 方法提交一个会抛出运行时异常的任务,不捕获异常。

第三种:submit 或者 execute 提交,让线程池饱和之后抛出拒绝异常,代码没有捕获异常。

第一种情况,无论如何都不会触发 dispatchUncaughtException 方法。因为 submit 方法提交,不论你捕获与否,源码里面都帮你捕获了:

第二种情况,如果不捕获异常,会触发 dispatchUncaughtException 方法,因为 runWorker 方法的源码里面虽然捕获了异常,但是又抛出去了:

而我们自己没有捕获,所以会触发 dispatchUncaughtException 方法。

第三种情况,和第二种其实是一样的。没有捕获,就会触发。

那么我现在给你一段这样的代码:

你肯定知道这是会抛出异常的吧。

就像这样式儿的:

我们完全没有打印日志的代码吧?

那你现在知道控制台这个异常信息是怎么来的了不?

是不是平时根本就没有注意这个点。

文章分享自微信公众号:
why技术

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

作者:why技术
原始发表时间:2020-09-20
如有侵权,请联系 cloudcommunity@tencent.com 删除。
登录 后参与评论
0 条评论

相关文章

  • 进阶spring/Hibernate*框架精选面试题

    ,今天给大家带来的是一些Java框架的面试题,这些面试题涵盖Hibernate框架,spring框架等,建议收藏

    框架师
  • 「建议心心」要就来15道多线程面试题一次爽到底(1.1w字用心整理)

    进程(process)和线程(thread)是操作系统的基本概念,但是它们比较抽象,不容易掌握。 最近在阮一峰的博客上看到了一个解释,感觉非常的好,分享给小伙伴...

    鹿老师的Java笔记
  • Android-Threadpool

         关于线程我不想再说什么,感兴趣的同学可以看我之前写过的一篇文章:Android-多线程,这里对线程有一个比较详细的解释。

    android_薛之涛
  • 有的线程它死了,于是它变成一道面试题

    我记得那天是周一,刚刚经历过周末过的放松,干劲十足的我正在键盘上疯狂的输出。这时,我的手机响了起来,拿起一看,是来自杭州的电话,心想这次是要给我推荐股票呢还是要...

    why技术
  • Java集合遍历时遇到的坑

    面试题中可能会被问到对Java集合的了解情况,并深入集合底层的源码,以及使用集合的时候有没有遇到坑——这时候其实是想考察大家在日常工作中是否细心,一般不建议说,...

    黑洞代码
  • 终于,我也要出一本C#的书了 - 我的写作历程与C#书单推荐

    我于2012年3月开始工作,到现在马上就满六年了。这六年里,我从一个连Sql server是什么都不知道,只会写最简单的c#的程序员开始做起,一步一步从一个籍籍...

    s055523
  • Java集合中的Queue & Deque

    Java 中的 这个 Queue 接口稍微有点坑,一般来说队列的语义都是先进先出(FIFO)的。

    Vincent-yuan
  • Collections.synchronizedMap和Hashtable

    在SynchronizedMap内部维护了一个普通对象Map,还有排斥锁mutex,如图

    名字是乱打的
  • 夯实Java基础系列10:深入理解Java中的异常体系

    本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看

    Java技术江湖
  • 近期遇到的关于 Python 的面试题

    前段时间去面试了一些招聘 Python 软件开发的公司,看看现在的公司都关注 Python 的那些方面。因为疫情原因,现在面试都是电话或者视频面试,也可以约晚上...

    somenzz
  • Java岗大厂面试百日冲刺 - 日积月累,每日三题【Day2】 —— Redis篇1

      本栏目Java开发岗高频面试题主要出自以下各技术栈:Java基础知识、集合容器、并发编程、JVM、Spring全家桶、MyBatis等ORMapping框架...

    陈哈哈
  • dotnet 代码调试方法

    本文将会从简单到高级,告诉大家如何调试 dotnet 的代码,特别是桌面端。本文将会使用到 VisualStudio 大量的功能,通过各种好用的功能提高调试方法

    林德熙
  • java基础学习_IO流01_异常、File类_day19总结

    黑泽君
  • 「建议心心」要就来15道多线程面试题一次爽到底(1.1w字用心整理)

    进程(process)和线程(thread)是操作系统的基本概念,但是它们比较抽象,不容易掌握。 最近在阮一峰的博客上看到了一个解释,感觉非常的好,分享给小伙伴...

    鹿老师的Java笔记
  • 最新 Java 核心技术教程,都在这了!

    以下是Java技术栈微信公众号发布的所有关于 Java 的技术干货,2021最新更新版,本文会长期更新。

    Java技术栈
  • Java中实现多线程的3种方法介绍和比较

    黄小怪
  • 124道全面且精准的大厂Java面试题分享

    金九银十到了程序员们的跳槽季,大家是不是都在紧张的准备着呢,本文准备了124道Java面试题分享给大家,由于文章篇幅的限制文中没有带答案哦,但是答案可以免费分享...

    Java周某人
  • 要不来重新认识Spring事务?三歪又学到了

    从唯一性说起 写了十几年代码,直到现在,我见过非常多的处理唯一性约束的方法都是放在代码里,而非数据库里。 直到现在我也一直很困惑,这些人为什么不使用数据库...

    Java3y
  • Spring MVC面试题(2021最新版)

    小编分享的这份Java后端开发面试总结包含了JavaOOP、Java集合容器、Java异常、并发编程、Java反射、Java序列化、JVM、Redis、Spri...

    Java程序猿

扫码关注腾讯云开发者

领取腾讯云代金券