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

6次阅读

共计 5801 个字符,预计需要花费 15 分钟才能阅读完成。

这里 why 的第 66 篇原创文章

一道面试题

我一年前写过这篇文章《有的线程它死了,于是它变成一道面试题》,这是晚期作品,遣词造句,排版行文都有一点稚嫩,然而不知咋地,还是有很多人看过。

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

所以我前面应该会重写一下,翻新翻新,再补充一点新的货色进去。

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

一个线程池中的线程异样了,那么线程池会怎么解决这个线程?

这个题是我遇到的一个实在的面试题,过后并没有答复的很好。而后通过下面的文章,我在源码中寻找到了答案。

先给大家看两个案例。

当执行形式是 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 办法。

为什么这样说呢?

咱们当初把状况分为三种。

第一种:submit 办法提交一个会抛出运行时异样的工作,捕不捕捉异样都能够。

第二种:execute 办法提交一个会抛出运行时异样的工作,不捕捉异样。

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

第一种状况,无论如何都不会触发 dispatchUncaughtException 办法。因为 submit 办法提交,不管你捕捉与否,源码外面都帮你捕捉了:

第二种状况,如果不捕捉异样,会触发 dispatchUncaughtException 办法,因为 runWorker 办法的源码外面尽管捕捉了异样,然而又抛出去了:

而咱们本人没有捕捉,所以会触发 dispatchUncaughtException 办法。

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

那么我当初给你一段这样的代码:

你必定晓得这是会抛出异样的吧。

就像这样式儿的:

咱们齐全没有打印日志的代码吧?

那你当初晓得控制台这个异样信息是怎么来的了不?

是不是平时基本就没有留神这个点。

最初说一句(求关注)

还记得我之前这篇文章中的一个对话截图吗:

写这篇文章的时候我又问了她近况,她曾经不在乎这些技术问题了,因为她从程序媛摇身一变,变成了产品汪。从需要实现方变成了需要输出方,真是一个富丽的转身啊:

满腹经纶,难免会有纰漏,如果你发现了谬误的中央,能够在留言区提出来,我对其加以批改。感谢您的浏览,我保持原创,非常欢送并感谢您的关注。

我是 why,一个被代码耽搁的文学创作者,不是大佬,然而喜爱分享,是一个又暖又有料的四川好男人。

正文完
 0