JVM关闭的时候到底会不会等待线程池线程任务执行完毕

Posted wen-pan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM关闭的时候到底会不会等待线程池线程任务执行完毕相关的知识,希望对你有一定的参考价值。


  • 线程池的shutdown()方法和shutdownNow()方法起到的作用只是将每个线程内部的中断状态变为true,表示该线程收到过中断信号。并不能实际的停止线程,也就是说只能够起到一个通知的作用!
  • 其实这个问题的答案很容易知道,反向想一想,如果JVM关闭的时候如果真的需要等待每一个正在执行任务的线程执行完毕才完全关闭,那么如果有的任务执行非常耗时(或者直接就是死循环),那岂不是JVM永远不能退出了。
  • 这里主要是探究在JVM关闭过程中的动作,以及如果关闭过程中存在一直运行的任务会如何处理。
  • shutdown + shutdownNow 原理见 线程池shutdown和shutdownNow原理和区别

I)、问题产生原因

  1. 在一次翻代码的时候发现在项目中大家使用的最多的两种线程池是ThreadPoolExecutorThreadPoolTaskExecutor
  2. 常见的线程池的两种使用方式
    • 一般经常使用的线程池我们会把它注入到spring容器中,让容器帮我们创建和管理线程池避免线程池的频繁创建和关闭
    • 如果不经常使用的线程池,比如某些job半年运行一次,每次运行的时候需要使用多线程去执行。鉴于这种场景下的线程池不会被频繁使用到,所以我们会考虑通过局部变量的方式自己new一个线程池,然后在任务执行完毕的时候手动调用shutdown方法去关闭线程池,以达到节约资源的目的。
  3. 在代码里看见有同事使用线程池执行任务的时候,在代码上添加了一行注释// 在线程池关闭的时候会等待这里的任务执行完毕才会退出。这里我感到比较疑惑。
    • 难道关闭JVM的时候真的要等到已经在执行的每个线程任务都执行完?
    • 要是有的线程任务是死循环或执行非常耗时呢?那JVM岂不是关闭不了?

II)、问题思考

由上面几点问题引发了对线程的一些思考和疑问

  1. 我们注入到spring容器的线程池,在JVM关闭的时候到底会不会调用线程池的shutdown方法,从而实现优雅的关机
    • 如果会调用,那么是在哪里进行调用的?
    • 如果不会调用,那么JVM一定会等到线程池中正在执行的任务执行完毕后再完全退出吗?
    • 结论:不会调用shutdown方法,JVM关闭时不会等到线程任务执行完毕后再关机,会强制kill掉,并且不抛异常
  2. 自己new的局部线程池如果不手动调用shutdown方法,那么在JVM关闭的时候会不会调用shutdown方法
    • 如果会,那么是在哪里被吊起的?
    • 如果不会,那么JVM一定会等到线程池中正在执行的任务执行完毕后再完全退出吗?
    • 结论:不会调用shutdown方法,JVM关闭时不会等到线程任务执行完毕后再关机,会强制kill掉,并且不抛异常
  3. 所谓的 局部线程池如果不手动调用shutdown方法可能会造成资源浪费,到底怎么造成的?

III)、问题分析和验证

在最开始分析验证这个问题的时候尝试了很多种方式,后面经过验证发现直接使用main函数验证和web应用中的验证结果是一致的。

1、验证前准备

①、准备钩子

我们通过添加钩子函数来阻塞JVM关闭过程,以便于打印出JVM关闭时线程池的状态,来验证我们的结果。为了使用方便,我们将添加钩子的代码封装一下。

/**
 * 添加钩子
 */
private static void addHook() {
    // 通过添加钩子函数来阻塞JVM关闭过程,以便于打印出JVM关闭时线程池的状态
    Runtime.getRuntime().addShutdownHook(new Thread(() -> {
        try {
            System.out.println("钩子执行,等待 5s JVM正式关闭......");
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            System.out.println("钩子函数睡眠被中断......");
        }
      	// 打印线程池信息
        System.out.println("线程池信息 :" + executor.toString());
    }));
}

2、验证不手动调用线程池shutdown方法,且在线程池已经没有可运行的任务时JVM关闭会不会关闭线程池

①、测试代码
// 手动创建一个线程池
static ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0,
 TimeUnit.SECONDS, new ArrayBlockingQueue<>(1), new ThreadPoolExecutor.DiscardPolicy());

// main函数测试
public static void main(String[] args) throws InterruptedException {
  test01();
}
/**
 * 测试:不手动调用线程池shutdown方法,在线程池已经没有可运行的任务时JVM关闭会不会关闭线程池(即调用shutdown方法)
 */
public static void test01() throws InterruptedException {
  // 通过添加钩子函数来阻塞JVM关闭过程,以便于打印出JVM关闭时线程池的状态
  addHook();

  executor.execute(() -> {
    System.out.println("我是子线程......");
  });

  // 主线程阻塞在这里,等到子线程执行完毕后,我们关闭JVM
  TimeUnit.SECONDS.sleep(30);
}
②、测试结果
我是子线程......
钩子执行,等待 5s JVM正式关闭......
线程池信息 :java.util.concurrent.ThreadPoolExecutor@4f536358[Running, pool size = 1, active threads = 0, queued tasks = 0, completed tasks = 1]

可以看到JVM关闭的时候并没有调用shutdown,此时线程池的状态仍然是Running

3、验证不手动调用线程池shutdown方法,但线程池有可运行的任务时JVM关闭会不会关闭线程池

①、测试代码
public static void test02() throws InterruptedException {
    // 通过添加钩子函数来阻塞JVM关闭过程,以便于打印出JVM关闭时线程池的状态
    addHook();

    // 提交任务到线程池,这个任务一直不结束并且不响应中断信号,在运行过程中直接关闭JVM
    executor.execute(() -> {
        while (true) {
            System.out.println("我是子线程......");
        }
    });

    // 主线程阻塞在这里,等到子线程执行完毕后,我们关闭JVM
    TimeUnit.SECONDS.sleep(30);
}
②、测试结果
我是子线程......
.......................
钩子执行,等待 5s JVM正式关闭......
.......................
我是子线程......
我是子线程......
线程池信息 :java.util.concurrent.ThreadPoolExecutor@5ba16227[Running, pool size = 1, active threads = 1, queued tasks = 0, completed tasks = 0]
我是子线程......
我是子线程......
.......................

通过执行结果我们可以看到,子线程一直在运行,当我们点击关闭JVM的时候,在钩子中打印了线程状态,此时线程池状态为Running,并且子线程仍然在执行,直到钩子方法执行完毕后JVM完全关闭时子线程才被终止,并且子线程被终止的时候不会抛出任何异常。

4、验证手动调用线程池shutdown方法,但线程池中有可运行的任务时JVM关闭会不会关闭线程池

调用shutdownNow方法结果也是一样的!!!
该验证代码可以说明:即使你调用了线程池的shutdown方法,JVM也不会等你执行完再关机,该把你干掉还是要把你干掉。

①、测试代码
public static void test03() throws InterruptedException {
    // 通过添加钩子函数来阻塞JVM关闭过程,以便于打印出JVM关闭时线程池的状态
    addHook();

    // 提交任务到线程池,这个任务一直不结束并且不响应中断信号,在运行过程中直接关闭JVM
    executor.execute(() -> {
        while (true) {
            System.out.println("我是子线程......");
        }
    });

    TimeUnit.MILLISECONDS.sleep(200);
  	// 手动调用shutdown关闭线程池(这里调用shutdownNow方法结果也是一样的)
    executor.shutdown();
    // 主线程阻塞在这里,等到子线程执行完毕后,我们关闭JVM
    TimeUnit.SECONDS.sleep(30);
}
②、测试结果
我是子线程......
.......................
钩子执行,等待 5s JVM正式关闭......
.......................
我是子线程......
我是子线程......
线程池信息 :java.util.concurrent.ThreadPoolExecutor@6a3a39a2[Shutting down, pool size = 1, active threads = 1, queued tasks = 0, completed tasks = 0]
我是子线程......
我是子线程......
.......................

可以看到此时线程池的状态为 Shutting down,表示正在关机中,但线程任务一直在运行,所以导致了线程池一直在关闭中。直到钩子方法执行完毕后JVM完全关闭时子线程才被终止,并且子线程被终止的时候不会抛出任何异常。

IV)、总结

通过上面的几个测试案例,可以得出如下结论:

  • 在JVM关机的时候如果没有手动关闭线程池,那么JVM关机时线程池的shutdown方法不会被吊起。
  • 在JVM关机的时候如果没有手动关闭线程池,并且此时线程池中已经没有正在执行的任务了,在JVM关闭的过程中也不会调用线程池的shutdown方法,此时线程池的状态仍然为running。直到JVM完全退出后线程池被完全释放。
  • 在JVM关机的时候如果没有手动关闭线程池,并且此时线程池中还有正在执行的任务(也就是正在执行的线程),那么该线程不会收到任何的中断通知,并且在JVM关闭的过程中会一直运行,直到JVM执行完关机前的钩子方法以后,这些正在运行的任务会强制被终止,并且不会抛出异常。
  • 以上案例【直接运行main函数测试】 和 【新建一个web应用程序将线程池注入到spring容器中测试】两者结果都一样,也就是说spring并没有做这样的工作(在JVM关机的时候调用线程池的shutdown方法去关闭线程池)
  • 需要注意的是,JVM关闭的时候不会对线程池中的线程发起中断信号,JVM是不会管这些事情的,线程池中的线程的中断信号是调用线程池的shutdown方法的时候去发起的中断信号。

所以,如果我们执行的方法非常耗时,但是我们期望在关机的时候JVM会等待线程任务执行完毕后再关机,这是不可能的。所以我们在设计代码的时候需要特别注意!!!

V)、优雅的关闭线程池

那线程池如何优雅的关闭呢,下面介绍几种方式供参考!

①、使用钩子来关闭

JVM关闭的时候会自动回调所有的钩子方法,并且要等钩子方法执行结束了才会继续执行关机,所以我们在钩子方法中也需要注意尽量不要写特别复杂或特别耗时的逻辑操作,免得造成JVM关闭花费太多时间。

//设置关闭钩子
Runtime.getRuntime().addShutdownHook(new Thread(){
  @Override
  public void run() {
    executor.shutdown();
    // 设定最大重试次数
    int retry = 2;
    try {
      // 每次等待 3 s,重试
      if (!executor.awaitTermination(3, TimeUnit.SECONDS) && retry-- > 0) {
        if (retry == 1){
          // 调用shutdownNow()取消正在执行的任务,SHUTDOWN->STOP是被允许的
          executor.shutdownNow();
        }else {
          log.error("线程池任务未正常执行结束");
        }
      }
    } catch (InterruptedException ie) {
      // 重新调用 shutdownNow
      executor.shutdownNow();
    }
  }
});

通过钩子函数来关闭线程池,参考 https://www.jianshu.com/p/45fe78ae2b3f

②、使用DisposableBean关闭

我们可以使用DisposableBean来关闭线程池,当系统关闭时对象被销毁会调用DisposableBean的destroy()方法,利用这个特性我们可以直接在destroy方法中调用线程池的shutdown方法

public interface DisposableBean {

   void destroy() throws Exception;

}

VI)、其他问题

问题1、

网上有人说如果我们不手动去shutdown(),那么将由finalize()方法在垃圾回收的时候被调用,并且在finalize()方法内部会shutdown线程池。这个结论对不对呢?

是对的,但是和我们分析的问题不一样。在系统进行垃圾回收的时候,如果一个对象(比如我们的线程池对象)已经确定要被回收了,那么在回收的最后一步会调用finalize方法,我们在线程池ThreadPoolExecutor中可以看到他确实覆写了Object类的finalize方法,并且在该类中也确实做了线程池关闭的动作。

但是我们这里讨论的是在JVM关闭的时候会不会调用线程池的shutdown方法,在JVM关闭的时候也不会进行最后一次垃圾回收,所以在JVM关闭的时候不会调用对象的finalize方法。这一点可以通过自己覆写一下Object类的finalize方法验证。

VII)、参考

  1. 深入Java线程池的执行和关闭流程
  2. shutdown(),shutdownNow(),awaitTermination(n, TimeUnit) 这三个方法的应用
  3. 线程池 多线程运行结束后 如何关闭? ExecutorService的正确关闭方法
  4. 深入JVM关闭与关闭钩子
  5. ThreadPoolExecutor 优雅关闭线程池的原理

以上是关于JVM关闭的时候到底会不会等待线程池线程任务执行完毕的主要内容,如果未能解决你的问题,请参考以下文章

如何优雅的关闭线程池?

线程池+协程+gevent模块

ThreadPoolExecutor 线程池

正确关闭线程池:shutdown 和 shutdownNow 的区别

Java线程池详解

Java线程池详解