多线程异常处理

多线程异常处理

在日常开发中,会经常启用多线程进行一些异步计算处理。而异常情况总是存在的,如果异步计算中出现了未检查异常而又没处理妥当的话,很有可能会导致异常丢失,从而难以定位问题。

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<String> {
    @Override
    public String call() throws Exception {
        System.out.println("运行...");
        throw new RuntimeException("未检查异常");
    }
}
@Test
public void futureTaskExceptionTest() throws Exception {
    FutureTask<String> 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<V> 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<Void>
        implements Runnable, AsynchronousCompletionTask {
    CompletableFuture<Void> dep; Runnable fn;
    AsyncRun(CompletableFuture<Void> 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<Void> 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
    }
}

转载于:https://my.oschina.net/bingzhong/blog/2980481

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值