Skip to main content

如何处理线程池任务运行异常?

作者:程序员马丁

在线博客:https://open8gu.com

note

大话面试,技术同学面试必备的八股文小册,以精彩回答应对深度问题,助力你在面试中拿个offer。

回答话术

当线程池通过 execute 方法提交任务后,线程池会创建新的工作线程来执行任务,或者已有的工作线程会领取任务并开始执行。如果在执行过程中抛出异常,这个异常不会传播到提交任务的线程,而是由线程池处理。

假如线程池此时仍然处于正常运行的状态,销毁该线程后,如果线程池中线程数小于核心或者最大线程数,等下次任务提交时会创建一个新的线程来替代它,以确保线程池中的线程数量不变。

如果线程数小于核心线程数,下次提交任务会直接创建新的线程。如果线程数大于核心线程数并且阻塞队列已满,但仍小于最大线程数,则会创建新的线程。

通常在处理线程池任务运行异常时,有多种解决方案,较为常用的方法是通过在任务代码中添加 try-catch 块进行异常捕获和处理。此外,我们还可以在任务执行前后添加异常处理逻辑,这样就可以根据不同的异常判断是重复投递任务、打日志还是报警等,以确保所有异常都能被正确处理,保证系统的稳定性和可靠性。

当线程池通过 submit 方法提交任务运行时,异常会被记录在 Future 对象中。当我们调用 Future.get 的时候,任务执行时抛出的异常会在这时被包装为一个 ExecutionException 抛出,此时我们可以主动进行捕获,并根据需要进行处理。

问题详解

1. 运行任务抛出异常后,错误日志哪来的?

当我们在线程池中抛出一个未被捕获的异常时,会打印相关的异常堆栈。大家谁有考虑过,这个日志谁怎么被打印出来的么。

ExecutorService executorService = Executors.newFixedThreadPool(1);
executorService.execute(() -> {
int i = 1 / 0;
});

// 大家有考虑过这个日志是怎么被输出的么?
/**
* Exception in thread "pool-1-thread-1" java.lang.ArithmeticException: / by zero
* at com.nageoffer.xxx.project.service.impl.xxx.lambda$main$6(ShortLinkServiceImpl.java:591)
* at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
* at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
* at java.base/java.lang.Thread.run(Thread.java:833)
*/

部分线程池源码,可以看到线程池内部只是将异常进行 throw 操作,异常信息是如何被打印的呢?

beforeExecute(wt, task);
try {
task.run();
afterExecute(task, null);
} catch (Throwable ex) {
afterExecute(task, ex);
throw ex;
}

向上抛出的异常会由 JVM 虚拟机进行调用 Thread#dispatchUncaughtException 进行处理。

/**
* 向处理程序分配一个未捕获的异常,dispatchUncaughtException 方法仅能被 JVM 调取
*/
private void dispatchUncaughtException(Throwable e) {
getUncaughtExceptionHandler().uncaughtException(this, e);
}

而这个处理未捕获异常的 "程序" 就是 UncaughtExceptionHandler,继续查看相关方法 Thread#getUncaughtExceptionHandler

public Thread.UncaughtExceptionHandler getUncaughtExceptionHandler() {
return uncaughtExceptionHandler != null ?
uncaughtExceptionHandler : group;
}

这里会查看线程是否有未捕获处理策略,没有则使用默认线程组的策略执行。我们查看下默认的 ThreadGroup#uncaughtException 是如何处理的?

public void uncaughtException(Thread t, Throwable e) {
if (parent != null) {
parent.uncaughtException(t, e);
} else {
Thread.UncaughtExceptionHandler ueh =
Thread.getDefaultUncaughtExceptionHandler();
if (ueh != null) {
ueh.uncaughtException(t, e);
} else if (!(e instanceof ThreadDeath)) {
// 最终会调用到这里将一场堆栈信息进行打印
System.err.print("Exception in thread \""
+ t.getName() + "\" ");
e.printStackTrace(System.err);
}
}
}

其实也就相当于将异常吞掉,但是会打印出异常具体的异常信息。到这里我们也能够明白,线程池中抛出的异常最终异常打印的处理逻辑在哪实现的了。

如果没有为线程指定异常拦截器,那么就会使用默认的拦截器,而默认的拦截器吞掉异常并且打印堆栈。

2. 线程池异常处理的三种方式

2.1. 在线程工厂中指定异常拦截器 UncaughtExceptionHandler

一般我们创建线程池时都会使用线程工厂, 在创建线程工厂时可以指定 UncaughtExceptionHandler 处理未捕获异常策略。

// 自定义异常处理器,在捕获异常后,输出线程名称和异常信息
ThreadFactory threadFactory = new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = Executors.defaultThreadFactory().newThread(r);
t.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
public void uncaughtException(Thread t, Throwable e) {
System.out.println("Thread " + t.getName() + " threw exception: " + e);
}
});
return t;
}
};
ExecutorService executorService = Executors.newFixedThreadPool(1, threadFactory);

// 提交一个会抛出异常的任务
executorService.execute(() -> {
int i = 1 / 0; // 这将会抛出 ArithmeticException
});

/**
* 运行结果:
* Thread pool-1-thread-1 threw exception: java.lang.ArithmeticException: / by zero
*/

解锁付费内容,👉 戳