Loading

36-线程池中线程抛出异常

1. 引入

一个线程池中的线程异常了,那么线程池会怎么处理这个线程?

public class ThreadPoolTest {

    public static void main(String[] args) throws Exception {
        ThreadPoolExecutor executor = init();
        executor.execute(() -> sayHi("execute"));
        Thread.sleep(1000);
        executor.submit(() -> sayHi("submit"));
    }
    
    private static ThreadPoolExecutor init() {
        return new ThreadPoolExecutor(
                3,
                10,
                60,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(3),
                Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()
        );
    }
    
    public static void sayHi(String name) {
        String printStr = "===> [thread-name] " + Thread.currentThread().getName() + " [type] " + name;
        System.out.println(printStr);
        throw new RuntimeException(printStr);
    }
}

从执行结果我们看出:当执行方式是 execute 时,可以看到堆栈异常的输出;当执行方式是 submit 时,堆栈异常没有输出。

怎么拿到 submit 的异常堆栈?

public static void main(String[] args) throws Exception {
    ThreadPoolExecutor executor = init();
    executor.execute(() -> sayHi("execute"));
    Thread.sleep(1000);
    Future<?> future = executor.submit(() -> sayHi("submit"));
    System.out.println("·································································");
    try {
        future.get();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

submit 方法执行时,返回结果封装在 Future 中,如果调用 Future#get 方法则必须进行异常捕获,从而可以抛出(打印)堆栈异常。

2. 抛出异常堆栈

2.1 execute

当一个线程因未捕获异常而即将终止时,JVM 将使用 Thread#dispatchUncaughtException(e) 获取已经设置的 UncaughtExceptionHandler 实例并通过调用其 uncaughtException() 方法而传递相关的异常信息。如果一个线程没有明确设置其 UncaughtExceptionHandler,则将其 ThreadGroup 对象作为它的 UncaughtExceptionHandler,如果 ThreadGroup 对象对异常没有什么特殊的要求,则 ThreadGroup 会直接转发给默认的未捕获异常处理器。

直接控制台打印异常堆栈:

2.2 submit

  1. 可以看到,submit() 把传进来的 task 封装成了一个 FutureTask,再把 FutureTask 传给 execute 进行调用,然后直接返回了该 FutureTask;
  2. 所以,submit() 其本质也是调用了 execute() 方法,所以它还是回到 java.util.concurrent.ThreadPoolExecutor#runWorker 方法,进而调用 FutureTask#run 方法;
  3. c.call() 方法执行了提交进来的任务,所以在控制台打印输出并抛出了异常,且异常也被捕获了,但没有继续抛出去,而是调用了 setException() 方法,把异常保存起来。所以,才有的“submit 不会打印堆栈异常信息”的说法;
  4. setException 方法内使用 CAS 改变这个 FutureTask#state 为 EXCEPTIONAL;

最后两个代码段为调用 FutureTask#get 方法关联代码。进入之后,s=state=3,然后调用 report() 方法。这个 report() 中的 outcome 就是任务抛出的异常。由于两个 if 都不满足,直接走到 122 行,抛出异常。在测试代码中,我们对异常进行了捕获,把堆栈信息打印了出来(不捕获,也还是会打印,因为抛给了 JVM)。

3. 不影响其他线程任务

3.1 code

public class ThreadPoolTest {
    public static void main(String[] args) throws Exception {
        ThreadPoolExecutor executor = init();
        for (int i = 0; i < 10; i++) {
            int finalI = i + 1;
            if (i % 2 == 0) {
                executor.execute(() -> sayHi(String.format("execute<%d>", finalI)));
            } else {
                executor.execute(() -> sayHi(String.format("exception<%d>", finalI)));
            }
        }
    }
    
    private static ThreadPoolExecutor init() {
        return new ThreadPoolExecutor(
                5,
                10,
                60,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(),
                Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()
        );
    }

    public static void sayHi(String name) {
        String printStr = "===> [thread-name] " + Thread.currentThread().getName() + " [type] " + name;
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (name.contains("exception")) {
            throw new RuntimeException(printStr);
        } else {
            System.out.println(printStr);
        }
    }
}

控制台打印:

3.2 debug

这个线程会再被放回线程池吗?

4. 小结

当一个线程池里面的线程异常后

(1)当执行方式是 execute 时,可以看到堆栈异常的输出;

ThreadPoolExecutor.runWorker() 方法中 task.run() 即执行我们的方法时,如果异常的话会 throw x; 所以可以看到异常。

(2)当执行方式是 submit 时,堆栈异常没有输出。但是调用 Future#get() 方法时,可以捕获到异常;

ThreadPoolExecutor.runWorker() 方法中 task.run() 其实还会继续执行 FutureTask#run() 方法,再在此方法中 c.call() 调用我们的方法,如果报错会 setException(),并不会抛出异常。当我们去 get() 时,才会将异常抛出。

(3)不会影响线程池里面其他线程的正常执行;线程池会把这个线程移除掉,并创建一个新的线程放到线程池中。

当线程异常,会调用 ThreadPoolExecutor.runWorker() 方法最后面的 finally 中的 processWorkerExit(),会将此线程 remove 并重新 addworker() 一个线程。

execute 源码执行流程

  1. 开始执行任务,新增或者获取一个线程去执行任务(比如刚开始是新增 coreThread 去执行任务),执行到 task.run() 时会去执行提交的任务。 如果任务执行失败,或 throw x 抛出异常;
  2. 之后会到 finally 中的 afterExecute() 扩展方法,我们可以扩展该方法对异常做些什么;
  3. 之后因为线程执行异常会跳出 runWorker() 的外层循环,进入到 processWorkerExit() 方法,此方法会将执行任务失败的线程删除,并新增一个线程
  4. 之后会到 ThreadGroup#uncaughtException() 方法,进行异常处理。如果没有通过 setUncaughtExceptionHandler() 方法设置默认的 UncaughtExceptionHandler,就会在 uncaughtException() 方法中打印出异常信息。

submit 源码执行流程

  1. 将传进来的任务封装成 FutureTask,同样走 execute 的方法调用,然后直接返回 FutureTask;
  2. 开始执行任务,新增或者获取一个线程去执行任务(比如刚开始是新增 coreThread 去执行任务);
  3. 执行到 task.run() 时,因为是 FutureTask,所以会去调用 FutureTask#run()
  4. FutureTask#run() 中,c.call() 执行提交的任务。如果抛出异常,并不会 throw x,而是 setException() 保存异常;
  5. 当我们阻塞获取 submit() 方法结果时 get(),才会将异常信息抛出。当然因为 runWorker() 没有抛出异常,所以并不会删除线程
posted @ 2022-08-01 09:38  tree6x7  阅读(1077)  评论(0编辑  收藏  举报