c语言 多线程 错误捕获,多线程异常处理 - 冰钟的个人空间 - OSCHINA - 中文开源技术交流社区...

多线程异常处理

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

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

}

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值