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

目录

一、验证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()方法时,可以捕获到异常,不会把这个线程移除掉,也不会创建新的线程放入到线程池中。

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

相关推荐
0白露4 分钟前
Apifox Helper 与 Swagger3 区别
开发语言
Tanecious.1 小时前
机器视觉--python基础语法
开发语言·python
叠叠乐1 小时前
rust Send Sync 以及对象安全和对象不安全
开发语言·安全·rust
战族狼魂2 小时前
CSGO 皮肤交易平台后端 (Spring Boot) 代码结构与示例
java·spring boot·后端
Tttian6223 小时前
Python办公自动化(3)对Excel的操作
开发语言·python·excel
xyliiiiiL3 小时前
ZGC初步了解
java·jvm·算法
杉之3 小时前
常见前端GET请求以及对应的Spring后端接收接口写法
java·前端·后端·spring·vue
hycccccch3 小时前
Canal+RabbitMQ实现MySQL数据增量同步
java·数据库·后端·rabbitmq
独好紫罗兰4 小时前
洛谷题单2-P5713 【深基3.例5】洛谷团队系统-python-流程图重构
开发语言·python·算法
天天向上杰5 小时前
面基JavaEE银行金融业务逻辑层处理金融数据类型BigDecimal
java·bigdecimal