多线程异常处理
在日常开发中,会经常启用多线程进行一些异步计算处理。而异常情况总是存在的,如果异步计算中出现了未检查异常而又没处理妥当的话,很有可能会导致异常丢失,从而难以定位问题。
Thread 未检查异常处理
Thread 是多线程调用的一个基础,所以首先看看 Thread 是如何处理未检查异常的。
在类 Thread 中,有一个 UncaughtExceptionHandler 接口,该接口用于定义 Thread 如何处理未检查异常。
@FunctionalInterface
public interface UncaughtExceptionHandler {
/**
* 定义如何处理线程中未捕获的异常。
* 该方法中抛出的异常都会被忽略
* @param t 线程
* @param e 异常
*/
void uncaughtException(Thread t, Throwable e);
}
以下为使用例子。先定义一个会抛出运行时异常的任务:
public class ExceptionTask implements Runnable {
@Override
public void run() {
System.out.println("运行...");
throw new RuntimeException("未检查异常");
}
}
有以下调用:
@Test
public void threadExceptionTest() throws Exception {
Thread thread = new Thread(new ExceptionTask());
// 用日志记录下错误
thread.setUncaughtExceptionHandler((thread, e) -> LOGGER.error("线程[{}]发生异常", t.getName(), e));
thread.start();
}
当线程开始运行,任务抛出一个未检查异常时,将会把错误信息记录到日志中。
而如果没有设置 UncaughtExceptionHandler ,那么 Thread 会如何处理未检查异常呢?
在类 Thread 中定义了方法 getUncaughtExceptionHandler ,通过该方法获取 UncaughtExceptionHandler 处理未检查异常。该方法的定义如下:
public UncaughtExceptionHandler getUncaughtExceptionHandler() {
return uncaughtExceptionHandler != null ?
uncaughtExceptionHandler : group;
}
如果 Thread 对象的 uncaughtExceptionHandler 属性为空 ,那么将返回 group ,group 是 ThreadGroup ,即该 Thread 对象的线程组。ThreadGroup 实现了 UncaughtExceptionHandler 接口,即其本身就是一个 UncaughtExceptionHandler ,它的未捕获异常处理方法实现如下:
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);
}
}
}
如果没有找到合适的 UncaughtExceptionHandler ,那么异常将会打印到控制台中,这样在实际开发中会导致难以发现异常的发生。
而如果觉得为每个 Thread 对象设置 UncaughtExceptionHandler 是麻烦的,那么有一个一劳永逸的方法,那就是设置线程的默认 UncaughtExceptionHandler 。
Thread.setDefaultUncaughtExceptionHandler((t, e) -> LOGGER.error("线程[{}]发生异常", t.getName(), e));
FutureTask 未检查异常处理
多线程任务除了实现 Runnable 接口外,还可以实现 Callable ,将其包装为 FutureTask ,可以在异步执行完成后,获取到返回值。有以下例子:
public class ExceptionCallable implements Callable {
@Override
public String call() throws Exception {
System.out.println("运行...");
throw new RuntimeException("未检查异常");
}
}
@Test
public void futureTaskExceptionTest() throws Exception {
FutureTask task = new FutureTask<>(new ExceptionCallable());
Thread thread = new Thread(task);
thread.setUncaughtExceptionHandler((t, e) -> LOGGER.error("线程[{}]发生异常", t.getName(), e));
thread.start();
}
运行后,会发现没有异常信息打印到日志中,即 UncaughtExceptionHandler 并没有起到作用。这是由于 FutureTask 内部已经对异常进行了处理。以下为 FutureTask 的 run 方法源码:
public void run() {
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
Callable c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
// FutureTask 自己捕获了在运行期间抛出的异常
try {
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
// 保存在执行期间抛出的异常
setException(ex);
}
if (ran)
set(result);
}
} finally {
runner = null;
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
由于 UncaughtExceptionHandler 是用于处理线程任务执行时(即 run 方法的调用)抛出的未处理异常。但是 FutureTask 已经在 run 方法内部捕获了异常并进行了处理。
FutureTask 的 setException 方法实现:
protected void setException(Throwable t) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
// outcome 属性为 FutureTask 的响应结果,即 get 方法返回的对象
outcome = t;
UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // 该任务的最终状态,即为异常状态
finishCompletion();
}
}
最后当调用 FutureTask 的 get 方法时,会调用 report 方法,将之前捕获的异常封装为 ExecutionException 并抛出。
private V report(int s) throws ExecutionException {
Object x = outcome;
if (s == NORMAL)
return (V)x;
if (s >= CANCELLED)
throw new CancellationException();
// 目前 FutureTask 的状态为 EXCEPTIONAL ,所以最后执行该逻辑
throw new ExecutionException((Throwable)x);
}
所以如果以 FutureTask 作为任务进行异步调用,如果最后没有调用 get 方法,异常将会丢失。
线程池
对于多线程的使用,一般来说是以线程池的方式去使用。
线程池内部还是使用 Thread 来实现多线程任务,那么对于异常的处理方式也是和上述提到的一样,设置 UncaughtExceptionHandler 。
线程池的线程,都是通过 ThreadFactory 获取的,需要为线程池的线程设置 UncaughtExceptionHandler ,必须自己实现 ThreadFactory ,并作为线程池的构造参数。
public class CustomThreadFactory implements ThreadFactory {
private final AtomicInteger count = new AtomicInteger(1);
private static final String THREAD_NAME_TEMPLATE = "custom-thread-%d";
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, String.format(THREAD_NAME_TEMPLATE, count.getAndIncrement()));
thread.setUncaughtExceptionHandler((t, e) -> LOGGER.error("线程[{}]发生异常", t.getName(), e));
return thread;
}
}
除此之外,还可以使用 apache commons 包的 BasicThreadFactory ,更为方便的构造一个 ThreadFactory 。
ThreadFactory threadFactory = new BasicThreadFactory.Builder()
.namingPattern("custom-thread-%d")
.uncaughtExceptionHandler((t, e) -> LOGGER.error("线程[{}]发生异常", t.getName(), e))
.build();
最后需要注意的是,线程池有两个提交任务的方法,分别为 execute 方法和 submit 方法。
execute 方法是提交一个 Runnable 任务,所以 UncaughtExceptionHandler 能够处理到任务执行时抛出的未检查异常。但是 submit 会将任务封装为 FutureTask ,上面提到,以这种方式启用多线程任务必须要在后续调用 FutureTask 的 get 方法才可以捕获处理到异常。
所以在使用线程池时,需要按照 execute 方法和 submit 方法的使用场景去选择使用哪一种任务的提交方式,以免发生如异常丢失的错误情况。
CompletableFuture
对于多线程的使用,在 Java 8 中提供了一种新的方式,即 CompletableFuture 。对于简单的异步调用,可以使用以下方法:
// task 为 Runnable 实现。使用内置的统一的线程池
CompletableFuture.runAsync(task);
这种方式会使用 CompletableFuture 内置的统一的线程池。也可以指定线程池:
CompletableFuture.runAsync(task, threadPoolExecutor);
但是以这样的方式进行异步调用,哪怕指定使用的线程池设置了 UncaughtExceptionHandler 也无法对未检查异常进行处理,将会丢失异常。
因为 CompletableFuture 和 FutureTask 一样,会自己在内部捕获任务执行中抛出的异常。CompletableFuture 会将提交给其处理的 Runnable 对象封装为 AsyncRun 对象,其源码实现如下:
static final class AsyncRun extends ForkJoinTask
implements Runnable, AsynchronousCompletionTask {
CompletableFuture dep; Runnable fn;
AsyncRun(CompletableFuture dep, Runnable fn) {
this.dep = dep; this.fn = fn;
}
public final Void getRawResult() { return null; }
public final void setRawResult(Void v) {}
public final boolean exec() { run(); return true; }
public void run() {
CompletableFuture d; Runnable f;
if ((d = dep) != null && (f = fn) != null) {
dep = null; fn = null;
if (d.result == null) {
// 当执行任务时发生异常,将会在内部捕获并处理
try {
f.run();
d.completeNull();
} catch (Throwable ex) {
d.completeThrowable(ex);
}
}
d.postComplete();
}
}
}
那么采用 CompletableFuture 去实现多线程调用的话,如何处理未检查异常呢?其实 CompletableFuture 捕获的异常,能够以函数式的方式去处理。如下:
CompletableFuture.runAsync(task, threadPoolExecutor).exceptionally(ex -> {
LOGGER.error("发生异常", ex);
return null;
});
Spring 中的异步调用
除了上面提到的方式,Spring 也有自己的异步调用的方式,那就是使用 @Async 。其底层实现为线程池,默认使用的是 Spring 内部的线程池,这个线程池已经设置好会处理未检查异常。
而如果想要使用自定义的线程池的话,有两种方式,一种是在 @Async 中指定线程池 Bean 。如果 @Async 指定的线程池已经设置好了 UncaughtExceptionHandler ,那么通过 Spring 的机制进行异步调用也是会正确处理未检查异常的。
另外一种方式是实现 AsyncConfigurer 。
@Configuration
public class AsyncExecutorConfiguration implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
// 构建自定义的线程池
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
// 线程池的 UncaughtExceptionHandler
}
}