36-线程池中线程抛出异常
1. 引入
一个线程池中的线程异常了,那么线程池会怎么处理这个线程?
public class ThreadPoolTest {
public static void main(String[] args) throws Exception {
ThreadPoolExecutor executor = init();
executor.execute(() -> sayHi("execute"));
Thread.sleep(1000);
executor.submit(() -> sayHi("submit"));
}
private static ThreadPoolExecutor init() {
return new ThreadPoolExecutor(
3,
10,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()
);
}
public static void sayHi(String name) {
String printStr = "===> [thread-name] " + Thread.currentThread().getName() + " [type] " + name;
System.out.println(printStr);
throw new RuntimeException(printStr);
}
}
从执行结果我们看出:当执行方式是 execute 时,可以看到堆栈异常的输出;当执行方式是 submit 时,堆栈异常没有输出。
怎么拿到 submit 的异常堆栈?
public static void main(String[] args) throws Exception {
ThreadPoolExecutor executor = init();
executor.execute(() -> sayHi("execute"));
Thread.sleep(1000);
Future<?> future = executor.submit(() -> sayHi("submit"));
System.out.println("·································································");
try {
future.get();
} catch (Exception e) {
e.printStackTrace();
}
}
submit 方法执行时,返回结果封装在 Future 中,如果调用 Future#get 方法则必须进行异常捕获,从而可以抛出(打印)堆栈异常。
2. 抛出异常堆栈
2.1 execute
当一个线程因未捕获异常而即将终止时,JVM 将使用 Thread#dispatchUncaughtException(e)
获取已经设置的 UncaughtExceptionHandler 实例并通过调用其 uncaughtException()
方法而传递相关的异常信息。如果一个线程没有明确设置其 UncaughtExceptionHandler,则将其 ThreadGroup 对象作为它的 UncaughtExceptionHandler,如果 ThreadGroup 对象对异常没有什么特殊的要求,则 ThreadGroup 会直接转发给默认的未捕获异常处理器。
直接控制台打印异常堆栈:
2.2 submit
- 可以看到,
submit()
把传进来的 task 封装成了一个 FutureTask,再把 FutureTask 传给 execute 进行调用,然后直接返回了该 FutureTask; - 所以,
submit()
其本质也是调用了execute()
方法,所以它还是回到java.util.concurrent.ThreadPoolExecutor#runWorker
方法,进而调用 FutureTask#run 方法; c.call()
方法执行了提交进来的任务,所以在控制台打印输出并抛出了异常,且异常也被捕获了,但没有继续抛出去,而是调用了 setException() 方法,把异常保存起来。所以,才有的“submit 不会打印堆栈异常信息”的说法;- setException 方法内使用 CAS 改变这个 FutureTask#state 为 EXCEPTIONAL;
最后两个代码段为调用 FutureTask#get 方法关联代码。进入之后,s=state=3,然后调用 report() 方法。这个 report() 中的 outcome 就是任务抛出的异常。由于两个 if 都不满足,直接走到 122 行,抛出异常。在测试代码中,我们对异常进行了捕获,把堆栈信息打印了出来(不捕获,也还是会打印,因为抛给了 JVM)。
3. 不影响其他线程任务
3.1 code
public class ThreadPoolTest {
public static void main(String[] args) throws Exception {
ThreadPoolExecutor executor = init();
for (int i = 0; i < 10; i++) {
int finalI = i + 1;
if (i % 2 == 0) {
executor.execute(() -> sayHi(String.format("execute<%d>", finalI)));
} else {
executor.execute(() -> sayHi(String.format("exception<%d>", finalI)));
}
}
}
private static ThreadPoolExecutor init() {
return new ThreadPoolExecutor(
5,
10,
60,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()
);
}
public static void sayHi(String name) {
String printStr = "===> [thread-name] " + Thread.currentThread().getName() + " [type] " + name;
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (name.contains("exception")) {
throw new RuntimeException(printStr);
} else {
System.out.println(printStr);
}
}
}
控制台打印:
3.2 debug
这个线程会再被放回线程池吗?
4. 小结
当一个线程池里面的线程异常后
(1)当执行方式是 execute 时,可以看到堆栈异常的输出;
ThreadPoolExecutor.runWorker()
方法中 task.run()
即执行我们的方法时,如果异常的话会 throw x;
所以可以看到异常。
(2)当执行方式是 submit 时,堆栈异常没有输出。但是调用 Future#get()
方法时,可以捕获到异常;
ThreadPoolExecutor.runWorker()
方法中 task.run()
其实还会继续执行 FutureTask#run()
方法,再在此方法中 c.call()
调用我们的方法,如果报错会 setException()
,并不会抛出异常。当我们去 get()
时,才会将异常抛出。
(3)不会影响线程池里面其他线程的正常执行;线程池会把这个线程移除掉,并创建一个新的线程放到线程池中。
当线程异常,会调用 ThreadPoolExecutor.runWorker()
方法最后面的 finally 中的 processWorkerExit()
,会将此线程 remove 并重新 addworker()
一个线程。
execute 源码执行流程
- 开始执行任务,新增或者获取一个线程去执行任务(比如刚开始是新增 coreThread 去执行任务),执行到
task.run()
时会去执行提交的任务。 如果任务执行失败,或throw x
抛出异常; - 之后会到 finally 中的
afterExecute()
扩展方法,我们可以扩展该方法对异常做些什么; - 之后因为线程执行异常会跳出
runWorker()
的外层循环,进入到processWorkerExit()
方法,此方法会将执行任务失败的线程删除,并新增一个线程; - 之后会到
ThreadGroup#uncaughtException()
方法,进行异常处理。如果没有通过setUncaughtExceptionHandler()
方法设置默认的 UncaughtExceptionHandler,就会在uncaughtException()
方法中打印出异常信息。
submit 源码执行流程
- 将传进来的任务封装成 FutureTask,同样走 execute 的方法调用,然后直接返回 FutureTask;
- 开始执行任务,新增或者获取一个线程去执行任务(比如刚开始是新增 coreThread 去执行任务);
- 执行到
task.run()
时,因为是 FutureTask,所以会去调用FutureTask#run()
; - 在
FutureTask#run()
中,c.call()
执行提交的任务。如果抛出异常,并不会throw x
,而是setException()
保存异常; - 当我们阻塞获取
submit()
方法结果时get()
,才会将异常信息抛出。当然因为runWorker()
没有抛出异常,所以并不会删除线程。