如何正确的关闭线程池

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

本文基于 OpenJDK11

1. 线程池简介

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

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

  • shutdown
  • shutdownNow
  • awaitTermination

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

现有如下任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Task implements Runnable {

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

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

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

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

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

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

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

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

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

2. 正确的关闭线程池

执行简单任务

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

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

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

1
2
3
4
5
任务开始执行
任务开始执行
主线程结束
任务执行结束
任务执行结束

执行复杂任务

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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("正常任务结束");
}
}

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

1
2
3
4
5
6
7
8
9
10
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("主线程结束");
}

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 方法配合使用。

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

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

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

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

文 / Rayjun

© 2020 Rayjun    PowerBy Hexo