您的位置:首页 > 其它

线程池源码-异常处理

2020-04-01 12:59 155 查看

在线程池源码系列文章 线程池源码-线程被全部关闭了吗 中有提到,线程池在结束 worker 线程时会有一个标识 completedAbruptly,用来判断线程是否为异常退出。

那什么时候线程会异常退出呢?答案很明显,在执行任务的过程中抛出了异常,且没有进行 try catch 处理。

本篇文章主要探索下面的问题

  • 线程池默认的异常处理方式,存在的问题
  • 关于异常处理的解决方案

默认处理

想知道线程池对异常的默认处理方式,我们先来看看下面的测试代码:

public static void main(String[] args){
ExecutorService threadPool = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
threadPool.execute(() -> {
System.out.println("current thread: " + Thread.currentThread().getName());
int res = 1 / 0;
});
}
}

运行结果,可以看到,控制台打印出了异常的堆栈信息:

线程池源码-任务执行 中有提到任务执行的过程,直接看到相关的核心代码:

final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
// 捕获异常并抛出
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}

通过阅读任务执行的源码,发现它 catch 住异常之后还会继续抛出,最终使用了 jvm 默认的异常处理方式,在控制台打印堆栈的异常信息。

使用默认的处理方式,显然是不切合实际的,被你 leader 看到这样处理异常,岂不把你打屎???在很多场景下,我们需要捕获这个异常,不管是进行特殊处理,还是记录日志,方便后续排查问题。

解决方案

内部捕获

这是最直接简单的方法,在任务执行的过程中捕获可能发生的异常。但同时你无法预知程序运行过程中可能发生的所有异常,当然你也可以用一个超级无敌大的 try {} catch{} 把所有代码都包住,确保万无一失,这种写法极其不合理且丑陋,代码这边就不赘述了。

FutureTask

JDK 提供了另外一种更优雅的处理方式,线程池提交任务还可以使用 submit() 方法,这个方法可以返回执行结果,包括异常信息。

threadPool.submit(() -> {
System.out.println("current thread: " + Thread.currentThread().getName());
Object object = null;
// 此处会抛空指针异常
System.out.print(object.toString());
});

通过 submit() 提交任务, Runnable 对象被一步封装成了 FutureTask 对象, FutureTask 对异常进行了特殊处理,可以通过 futureTask.get() 来获取任务执行过程中抛出的异常,后续的文章会对FutureTask 进行较为详细地解析,这边暂不详细赘述。

public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}

protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
return new FutureTask<T>(runnable, value);
}

修改一下测试代码:

public static void main(String[] args){
ExecutorService threadPool = Executors.newFixedThreadPool(5);
List<Future> futures = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Future future =  threadPool.submit(() -> {
System.out.println("current thread: " + Thread.currentThread().getName());
int res = 1 / 0;
});
futures.add(future);
}

// 捕获异常
futures.forEach(future -> {
try {
future.get();
} catch (Exception e) {
System.out.println("捕获异常信息:" + e.getMessage());
}
});
}

执行结果如下,可以看到异常堆栈被打印出来:

FutureTask 确实能够捕获子线程执行过程中抛出的异常,**但它需要显式地去遍历 Future 结果集,通过 future.get() 去获取程序异常,**似乎也不是太优雅的做法。

线程池接口扩展

线程池在任务执行完毕之后,会调用 afterExecute() 方法,可以看到其将运行结果,以及捕获的异常都作为参数传入方法中。

try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
// 捕获异常并抛出
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
// 任务执行完调用,无论是否发生异常
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}

而 afterExecute()方法默认是没有实现的,是线程池提供给外部的扩展接口。可以通过重写此方法,对异常进行统一处理。

class ExtendThreadPool extends ThreadPoolExecutor {

public ExtendThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}

@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
if (t != null) {
System.out.println("捕获异常信息:" + t.getMessage());
}
}
}

运行结果如下,异常也被统一处理了:

线程接口扩展

前面的做法是在线程池的纬度去做异常处理,那有没有办法从线程的纬度呢?

JDK 也提供了相关的接口,JVM 监控到某个线程因未捕获的异常退出时,会调用该线程预先设置的异常处理器,如果没有提供任何的异常处理器,那么默认的行为就是将堆栈信息输出到控制台。

要自定义线程池中的线程,可以通过继承 ThreadFactory 工厂类,另需要实现 Thread.UncaughtExceptionHandler 接口,进行定制化的异常处理,并将 handler 对象设置到创建完成的线程上。

class customThreadFactory implements ThreadFactory {
private ThreadFactory threadFactory = Executors.defaultThreadFactory();
private Thread.UncaughtExceptionHandler exceptionHandler;

public customThreadFactory(Thread.UncaughtExceptionHandler exceptionHandler) {
this.exceptionHandler = exceptionHandler;
}

public Thread newThread(Runnable r) {
Thread t = threadFactory.newThread(r);
t.setUncaughtExceptionHandler(exceptionHandler);
return t;
}
}

class ThreadExceptionHandler implements Thread.UncaughtExceptionHandler {

@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("捕获异常信息:" + e.getMessage());
}
}

public class MainTest {
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(5, 5,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
new customThreadFactory(new ThreadExceptionHandler()));
for (int i = 0; i < 5; i++) {
threadPool.execute(() -> {
System.out.println("current thread: " + Thread.currentThread().getName());
int res = 1 / 0;
});
}
}
}

运行结果如下:

总结

今天的内容主要涉及任务执行过程中的异常处理,除了提到线程池的默认处理方式,还分享了几种自定义处理方案:

  • 任务内部对异常进行捕获
  • FutherTask 捕获异常
  • 重写线程池的 afterExecute() 方法
  • 实现 Thread.UncaughtExceptionHandler 接口

希望通过本篇文章,你对子线程的异常处理有了更深刻的认识,如果觉得文章对你有帮助,欢迎留言点赞。

  • 点赞
  • 收藏
  • 分享
  • 文章举报
春娇的志明 发布了15 篇原创文章 · 获赞 0 · 访问量 142 私信 关注
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: