ThreadPoolExcutor 线程池 异常处理 (下篇)

时间:2022-03-02 16:17:58

前言

因为这是之前面试的一个题目,所以印象比较深刻,前几天写了一篇文章:ThreadPoolExcutor 线程池 异常处理 (上篇) 中已经介绍了线程池异常的一些问题以及一步步分析了里面的一些源代码,今天就来继续说下如何防范这种情况。

结论

这里直接抛出结论,然后再一个个分析:

  • 在我们提供的Runnable的run方法中捕获任务代码可能抛出的所有异常,包括未检测异常
  • 使用ExecutorService.submit执行任务,利用返回的Future对象的get方法接收抛出的异常,然后进行处理
  • 重写ThreadPoolExecutor.afterExecute方法,处理传递到afterExecute方法中的异常
  • 为工作者线程设置UncaughtExceptionHandler,在uncaughtException方法中处理异常 (不推荐)

分析解读

Runnable的run方法中捕获任务代码可能抛出的所有异常

这个其实最简单,但是往往面试官问这个问题 考察的点也不在这里。具体的方式可以参考我之前的一篇文章:论如何优雅的自定义ThreadPoolExecutor线程池

核心代码如下:

ThreadPoolExcutor 线程池 异常处理 (下篇)

使用ExecutorService.submit执行任务,利用返回的Future对象的get方法接收抛出的异常

1, 使用submit执行异步任务,然后通过Future的get方法来接收异常。演示如下:

冲图片可以看到,使用了get方法后,这里直接接收到了异常信息。

ThreadPoolExcutor 线程池 异常处理 (下篇)

2, 这里newTaskFor返回的是FutureTask,然后传递给了execute方法:

ThreadPoolExcutor 线程池 异常处理 (下篇)

3, 接着我们继续往下跟踪execute方法,发现这里调用的是ThreadExecutor中的execute方法,在ThreadPoolExcutor 线程池 异常处理 (上篇) 我们已经分析过这里,最终会到addWorker方法中执行线程的start()方法,因为我们在上一张图片传递的是FutureTask, 所以我们继续跟踪FutureTask中的run方法:

ThreadPoolExcutor 线程池 异常处理 (下篇)

4, 到了FutureTask.run() 方法中,一切似乎都已经明了,这里会有catch捕获当前线程抛出的异常,紧接着我们看看setException做了什么事情:

ThreadPoolExcutor 线程池 异常处理 (下篇)

5,setExcetion首先是将一个异常信息赋值给一个全局变量outcome,并且将全局的任务状态state字段通过CAS更新为3(异常状态)

然后最后做一些清理工作。

ThreadPoolExcutor 线程池 异常处理 (下篇)

6,finishCompletion后续是做一些线程池的清理工作,这里涉及到线程池以及线程池中的等待队列的操作,不清楚的同学可以看下线程池实现代码。到了这里线程池中的线程执行已经完毕了,下面再去跟踪一下FutureTask.get()方法。

ThreadPoolExcutor 线程池 异常处理 (下篇)

7,这里是FutureTask.get()的底层实现,这里其实会拿上面的setException方法中设置的outcome和state做一些逻辑判断,到了这里就直接往上抛出了异常,所以我们在最开始的main方法中才能够捕获到这个异常。

ThreadPoolExcutor 线程池 异常处理 (下篇)

重写ThreadPoolExecutor.afterExecute方法,处理传递到afterExecute方法中的异常

这里为何要重写afterExecute方法呢?因为线程执行完毕后一定会执行此方法,源码如下:

ThreadPoolExcutor 线程池 异常处理 (下篇)

所以我们可以重写此方法来达到接收异常的目的。

为工作者线程设置UncaughtExceptionHandler,在uncaughtException方法中处理异常 (不推荐)

1,我们在之前ThreadExecutor->Worker->run方法中直接往上抛出了异常,但是这些异常抛到哪里了呢?

ThreadPoolExcutor 线程池 异常处理 (下篇)

2,通过查询JVM的一些资料,最终的异常会到Thread.dispatchUncaughtException中,如下图:

ThreadPoolExcutor 线程池 异常处理 (下篇)

3,所以当我们自定义UncaughtExceptionHandler时就可以捕获到

ThreadPoolExcutor 线程池 异常处理 (下篇)

具体测试代码如下:

/**
* 测试singleThreadPool异常问题
*
* @author: wangmeng
* @date: 2019/3/25 23:40
*/
public class ThreadPoolException {
private final static Logger LOGGER = LoggerFactory.getLogger(ThreadPoolException.class); public static void main(String[] args) throws InterruptedException {
ExecutorService execute = new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), new ThreadFactoryBuilder().setUncaughtExceptionHandler(new MyHandler()).build()); execute.execute(new Runnable() {
@Override
public void run() {
LOGGER.info("=====11=======");
}
}); TimeUnit.SECONDS.sleep(5);
execute.execute(new Run1());
} private static class Run1 implements Runnable {
@Override
public void run() {
int count = 0;
while (true) {
count++;
LOGGER.info("-------222-------------{}", count); if (count == 10) {
System.out.println(1 / 0);
try {
} catch (Exception e) {
LOGGER.error("Exception",e);
}
} if (count == 20) {
LOGGER.info("count={}", count);
break;
}
}
}
}
} class MyHandler implements Thread.UncaughtExceptionHandler {
private final static Logger LOGGER = LoggerFactory.getLogger(MyHandler.class);
@Override
public void uncaughtException(Thread t, Throwable e) {
LOGGER.error("threadId = {}, threadName = {}, ex = {}", t.getId(), t.getName(), e.getMessage());
}
}

上面说了其实是不推荐重写UncaughtExceptionHandler 的,因为UncaughtExceptionHandler 只有在execute.execute()方法中才生效,在execute.submit中是无法捕获到异常的。

由于本人水平有限,文章中如果有不严谨的地方还请提出来,愿闻其详。