“线程池中线程异常后:销毁还是复用?”

目录

一、验证execute提交线程池中

测试

结论

二、验证submit提交线程池中

测试

结论

三、源码解析

查看submit方法的执行逻辑

查看execute方法的执行逻辑

为什么submit方法,没有创建新的线程,而是继续复用原线程?

四、总结


需要说明,本文的线程池都是java.util.concurrent.ExecutorService线程池,本文将围绕验证,阅读源码俩方面来解析这个问题。

一、验证execute提交线程池中

测试

我们首先定义了一个 ThreadPoolExecutorDeadTest 类,用来演示 Java 中 ThreadPoolExecutor 的使用,以及在任务执行过程中发生异常时线程池的行为。然后,在代码中,通过创建一个线程池并提交多个任务来模拟线程池的运行情况,其中一个任务故意抛出异常。

测试代码如下:

java 复制代码
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().setNameFormat("test-%s").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);
    }
  }
}

首先,调用 buildThreadPoolExecutor() 方法创建一个线程池并赋值给 executorService。这个线程池具有核心线程数 5,最大线程数 10,空闲线程存活时间 30 秒,任务队列容量为 1000,使用 CallerRunsPolicy 作为拒绝策略。

然后,调用 executorService.execute() 方法多次向线程池提交任务。任务类型为 Runnable,任务的内容是调用 exeTask 方法。

主线程在前五个任务提交后,调用 Thread.sleep(5000) 暂停5秒,以模拟任务执行间隔。在暂停后,向线程池再次提交五个任务,继续执行。

运行结果如下,

可以看到test-2线程不见了,出现了test-3线程。

结论

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

二、验证submit提交线程池中

测试

测试代码:

java 复制代码
public class ThreadPoolExecutorDeadTest {
  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"));
  }
  
  public static ExecutorService buildThreadPoolExecutor() {
    return new ThreadPoolExecutor(5, 10, 30, TimeUnit.SECONDS,
      new LinkedBlockingQueue<>(1000), new ThreadFactoryBuilder().setNameFormat("test-%s").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线程执行异常了,并没有抛出异常栈而且继续复用了,没有被移除。

结论

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

三、源码解析

查看 submit方法的执行逻辑

我们从上述测试代码中的executorService.submit(() -> exeTask("execute"));,进入到**java.util.concurrent.AbstractExecutorService#submit(java.lang.Runnable)方法,**可以看到,submit方法,也是包装了RunnableFuture后调用execute方法。

查看execute方法的执行逻辑

进入execute方法,然后进入execute方法的实现方法,就可以进入到java.util.concurrent.ThreadPoolExecutor#runWorker方法,

可以发现,run方法抛出异常之后,会执行finally。

然后,我们进入到java.util.concurrent.ThreadPoolExecutor#processWorkerExit方法,

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

为什么submit方法,没有创建新的线程,而是继续复用原线程?

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

进入 FutureTask的run方法,

可以发现包装的一层task,catch了异常,并没有往上抛。所以不会移除抛出异常的线程,创建新的线程。

但是,我们通过java.util.concurrent.FutureTask#get(),就可以获取对应的异常信息。

四、总结

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

  • 当执行方式是execute()时,可以看到堆栈异常的输出,线程池会把这个线程移除掉,并创建一个新的线程放到线程池中。

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

以上俩种执行方式,都不会影响线程池里面其他线程的正常执行。

相关推荐
Highcharts.js32 分钟前
Highcharts智能数据分析平台Orbit 多语言配置技术文档
java·前端·javascript·highcharts·orbit·智能数据分析·设置文档
Rain的Java大神之路41 分钟前
设计一套生产级 API 接口:从契约、安全、幂等到限流,我踩过的坑都在这了
java·后端·面试
SimonKing42 分钟前
零配置也能白嫖前沿大模型!dsh-our-free-model插件真香警告
java·后端·程序员
草莓熊Lotso43 分钟前
【C++ 基于微服务的即时通讯系统】文件存储子服务实战:UUID生成、文件IO封装与全接口单元测试
开发语言·网络·数据库·c++·微服务·架构·单元测试
nice_lcj52043 分钟前
【从零理解 Spring AI · 15】让检索更准:先宽召回,再精排序
java·后端·rag·spring ai·ai应用开发·检索优化·重排序
huaqianzkh2 小时前
WinForm C#:循环修改控件UI闪烁卡顿?SuspendLayout / ResumeLayout 与 BeginUpdate 优化
开发语言·ui·c#
杨凯凡2 小时前
Spring Boot 链路追踪实战:Micrometer、TraceId 与异步线程上下文传播
java·spring boot·后端
计算机毕设定制辅导-无忧学长2 小时前
《基于Spring Boot的迷游哈游戏综合服务平台的开发》
java·vue.js·spring boot·mysql·迷游哈游戏综合服务平台
非优秀程序员11 小时前
第04章 邮箱大战,163 与 QQ 的暗战|《互联网产品恩仇录》
java·vue.js·架构
知已难寻12 小时前
Docker 入门:容器技术与 Docker 基础
java·运维·服务器·docker·云原生·容器