前言

下面测试是使用的JUC包下的 java.util.concurrent.ThreadPoolExecutor,本文将围绕验证,阅读源码俩方面来解析这个问题。这里使用两种提交方式分别验证,两种提交方式不一样结果也不一样!如果连这个都不知道,别跟我说你用过线程池好吗?如果线上出bug异常被吞掉,不知道怎么排查你就老实了。。

1.验证execute提交线程池中
public class ThreadPoolExecutorDeadTest {

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = buildThreadPoolExecutor();
        executorService.execute(() -> exeTask("execute"));
        executorService.execute(() -> exeTask("execute"));
        executorService.execute(() -> exeTask("execute-exception"));
        executorService.execute(() -> exeTask("execute"));
        executorService.execute(() -> exeTask("execute"));


        Thread.sleep(5000);
        System.out.println("再次执行任务=======================");

        executorService.execute(() -> exeTask("execute"));
        executorService.execute(() -> exeTask("execute"));
        executorService.execute(() -> exeTask("execute"));
        executorService.execute(() -> exeTask("execute"));
        executorService.execute(() -> exeTask("execute"));
    }


    public static ExecutorService buildThreadPoolExecutor() {
        return new ThreadPoolExecutor(5, 10, 30, TimeUnit.SECONDS,
                                      new LinkedBlockingQueue<>(1000), new ThreadFactoryBuilder().setNamePrefix("test-").build()
                                      , new ThreadPoolExecutor.CallerRunsPolicy());
    }

    private static void exeTask(String name) {
        String printStr = "[thread-name:" + Thread.currentThread().getName() + ",执行方式:" + name + "]";
        if ("execute-exception".equals(name)) {
            throw new RuntimeException(printStr + ", 我抛异常了");
        } else {
            System.out.println(printStr);
        }
    }
}

运行后发现,test-2的线程不见了,多出来一个test-3线程

2025-09-22T05:40:47.png

结论:execute 提交到线程池的方式,如果执行中抛出异常,并且没有在执行逻辑中catch,那么会抛出异常,并且移除抛出异常的线程,创建新的线程放入到线程池中

2.验证submit提交线程池中
public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = buildThreadPoolExecutor();
        executorService.submit(() -> exeTask("execute"));
        executorService.submit(() -> exeTask("execute"));
        executorService.submit(() -> exeTask("execute-exception"));
        executorService.submit(() -> exeTask("execute"));
        executorService.submit(() -> exeTask("execute"));


        Thread.sleep(5000);
        System.out.println("再次执行任务=======================");

        executorService.submit(() -> exeTask("execute"));
        executorService.submit(() -> exeTask("execute"));
        executorService.submit(() -> exeTask("execute"));
        executorService.submit(() -> exeTask("execute"));
        executorService.submit(() -> exeTask("execute"));
}

运行结果,把异常吞掉了,外面一点感知都没没有!并且线程也继续复用了,这要是出bug了得排查多久?

2025-09-22T05:40:57.png

结论:submit 提交到线程池的方式,如果执行中抛出异常,并且没有catch,不会抛出异常,不会创建新的线程。

3.跟源码找原因

这还得了?要是我用了这个,线上出bug不完蛋啦?赶紧看看源码,看看什么原因!

3.1 查看submit执行方法java.util.concurrent.AbstractExecutorService#submit(java.lang.Runnable)

这里submit原来是包装了一层RunableFutrue然后调用了execute方法!

2025-09-22T05:41:05.png

3.2 查看execute方法的执行逻辑 java.util.concurrent.ThreadPoolExecutor#runWorker

2025-09-22T05:41:12.png

看看异常处理方法是啥

2025-09-22T05:41:16.png

可以发现,如果抛出异常,会移除抛出异常的线程,创建新的线程。

3.3 为什么上面刚开始测试的submit方法,没有创建新的线程,而是继续复用原线程?

还记得,我们在3.1的时候,发现submit也是调用了execute方法,但是在调用之前,包装了一层 RunnableFuture,那一定是在RunnableFuture的实现 FutureTask中有特殊处理了,我们查看源码可以发现。

原来如此!把异常自己吃了,放到ex变量里,等你get拿的时候给你跑异常,抛到主线程!

2025-09-22T05:41:24.png

2025-09-22T05:41:49.png

2025-09-22T05:41:54.png

2025-09-22T05:42:06.png

所以,只要使用的是submit,可以通过java.util.concurrent.FutureTask#get(),就可以获取对应的异常信息。

2025-09-22T05:42:35.png

这时候不知道你有没有发现什么问题?是不是把异常抛出了,结果主线程中其他的操作也没法执行被影响了?这时候就要根据业务进行优化了(自己去想,别等我给你写。。。)。好了,这次的分享先到这里,最近事情比较多,更新博客速度都变慢了,哈哈哈

总结一下:

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

  • 当执行方式是execute时,可以看到堆栈异常的输出,线程池会把这个线程移除掉,并创建一个新的线程放到线程池中。
  • 当执行方式是submit时,堆栈异常没有输出。但是调用Future.get()方法时,可以捕获到异常,不会把这个线程移除掉,也不会创建新的线程放入到线程池中。