如何正确的关闭线程池

线程池是 Java 开发中常用的组件之一,在应用程序关闭的时候,需要将线程池关闭。但关闭线程池时,要按照不同的情况来使用不同的方式。下面介绍几种关闭线程池的常用方法。

本文基于 OpenJDK11

1. 线程池简介

Java 中的线程池都实现了 Executor接口,这个接口提供了执行任务的入口。ExecutorService 继承了 Executor,提供了对线程池生命周期的管理,在开发中用的最多。

对于关闭线程池,ExecutorService 提供了三种方式:

  • shutdown
  • shutdownNow
  • awaitTermination

这几种方法都可以关闭线程池,但是每种方式之间有一些差别。

现有如下任务:

class Task implements Runnable {

    @Override
    public void run() {
        System.out.println("任务开始执行");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("任务执行结束");
    }
}

调用 shutdown方法之后,线程池就不会再接收新的任务,如果强行添加,就会报错。而线程池中的任务会继续执行,但是却不保证这些任务能够执行完成,具体原因,下文会说到。

ExecutorService threadPool = Executors.newFixedThreadPool(2);
threadPool.submit(new Task());
threadPool.shutdown();
threadPool.submit(new Task());

上面代码的执行结果如下:

java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@7e07db1f

调用 shutdownNow 方法之后,所以运行中的任务都会终止,而且会返回队列中等待的任务。

ExecutorService threadPool = Executors.newFixedThreadPool(1);
threadPool.submit(new Task());
threadPool.shutdownNow();

调用 awaitTermination 之后,线程池中的任务会继续运行,而且还可以接受新的任务。它是线程阻塞的,会等到任务执行完成才结束主线程,这是它与shutdown 的区别。当然也不会无限期的等下去,可以通过设置超时时间,在超时时间之后,就会结束线程的阻塞。

ExecutorService threadPool = Executors.newFixedThreadPool(1);
threadPool.submit(new Task());
threadPool.awaitTermination(6, TimeUnit.SECONDS);

2. 正确的关闭线程池

执行简单任务

还以上面的 Task 为例,这种 Task 不依赖外部的数据,执行这种任务的线程池在关闭时,只需要调用 shutdown 方法即可。

public static void main(String[] args) {
    ExecutorService threadPool = Executors.newFixedThreadPool(2);
    threadPool.submit(new Task());
    threadPool.submit(new Task());
    threadPool.shutdown();
    System.out.println("主线程结束");
}

这时程序的输出如下,任务会在主线程结束之后继续执行。

任务开始执行
任务开始执行
主线程结束
任务执行结束
任务执行结束

执行复杂任务

在执行一些复杂任务时,任务需要依赖外部的数据,任务为下:

class Task1 implements Runnable {
    public Map<String, String> data;
    public Task1(Map<String, String> o) {
        this.data = o;
    }
    @Override
    public void run() {
        while (data.containsKey("invoke")) {
            System.out.println("正常任务执行!");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("正常任务结束");
    }
}

在线程池中执行这个任务:

public static void main(String[] args) throws InterruptedException {
    ExecutorService threadPool = Executors.newFixedThreadPool(2);
    Map<String, String> data = new HashMap<>();
    data.put("invoke", "true");
    threadPool.submit(new Task1(data));
    Thread.sleep(1000);
    threadPool.shutdown();
    data.remove("invoke");
    System.out.println("主线程结束");
}

程序执行结果如下,在外部数据依赖删除之后,程序也就执行结束了。

正常任务执行!
主线程结束
正常任务结束

如果这个依赖是一些网络连接,或者数据库连接,在主程序退出之后,这些连接就会被销毁,那么线程池中的任务就无法继续执行。所以这个时候需要阻塞主线程,给线程池中的任务一些执行的时间。

public static void main(String[] args) throws InterruptedException {
    ExecutorService threadPool = Executors.newFixedThreadPool(2);
    Map<String, String> data = new HashMap<>();
    data.put("invoke", "true");
    threadPool.submit(new Task1(data));
    Thread.sleep(1000);
    threadPool.shutdown();
    try {
        if (!threadPool.awaitTermination(4, TimeUnit.SECONDS)) {
            System.out.println("任务还没结束");
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    data.remove("invoke");
    System.out.println("主线程结束");
}

调用了 awaitTermination 方法之后,主线程就会被阻塞,等待线程池中的任务继续执行,但是也不会无限期的等下去,等待超时时间过了之后,主程序还是会退出。

在关闭线程池时,一般是 shutdown 与 awaitTermination 方法配合使用。

threadPool.shutdown();
if (!threadPool.awaitTermination(5, TimeUnit.SECONDS)) {
    System.out.println("任务还没结束");
}

如果还想确认在线程池退出时,确保线程池中的任务全部结束,可以在阻塞时间过了之后使用 shutdownNow:

threadPool.shutdown();
if (!threadPool.awaitTermination(5, TimeUnit.SECONDS)) {
    System.out.println("任务还没结束");
    threadPool.shutdownNow();
}

这样就可以让线程池按照预想的结果关闭。

文 / Rayjun

©2024 Rayjun    PowerBy Hexo