异步调用(二)线程池的优雅关闭

一、线程中断

在介绍线程池关闭之前,先介绍下Thread的interrupt。

在程序中,我们是不能随便中断一个线程的,因为这是极其不安全的操作,我们无法知道这个线程正运行在什么状态,它可能持有某把锁,强行中断可能导致锁不能释放的问题;或者线程可能在操作数据库,强行中断导致数据不一致混乱的问题。正因此,Java里将 Threadstop() 方法设置为过时,以禁止大家使用。

那么一个线程什么时候可以退出呢?当然只有线程自己才能知道。

所以我们这里要说的 Threadinterrupt() 方法,本质不是用来中断一个线程。是将线程设置一个中断状态。当我们调用线程的interrupt方法,它有两个作用:

  • 如果此线程处于阻塞状态(比如调用了wait() 方法,io等待),则会立马退出阻塞,并抛出InterruptedException 异常,线程就可以通过捕获 InterruptedException 异常来做一定的处理,然后让线程退出。

  • 如果此线程正处于运行之中,则线程不受任何影响,继续运行,仅仅是线程的中断标记被设置为true。所以线程要在适当的位置通过调用 isInterrupted() 方法来查看自己是否被中断,并做退出操作。

注:

  1. 如果线程的interrupt方法先被调用,然后线程调用阻塞方法进入阻塞状态,InterruptedException 异常依旧会抛出。

  2. 如果线程捕获 InterruptedException 异常后,继续调用阻塞方法,将不再触发异常。

二、线程池的关闭方法

线程池提供了两个关闭方法:

1. shutdown()

停止接收新任务,原来的任务继续执行

  1. 停止接收新的submit的任务;
  2. 已经提交的任务(包括正在跑的和队列中等待的),会继续执行完成;
  3. 等到第2步完成后,才真正停止;
  4. 返回未执行的任务列表;

2. shutdownNow()

停止接收新任务,原来的任务停止执行

  1. 同 shutdown() 方法 ,先停止接收新submit的任务;
  2. 忽略队列里等待的任务;
  3. 尝试将正在执行的任务interrupt中断;
  4. 返回未执行的任务列表;

说明:它试图终止线程的方法是通过调用 Thread.interrupt() 方法来实现的,这种方法的作用有限,如果线程中没有sleepwaitCondition、定时锁等应用, interrupt() 方法是无法中断当前的线程的。所以,shutdownNow() 并不代表线程池就一定立即就能退出,它也可能必须要等待所有正在执行的任务都执行完成了才能退出。但是大多数时候是能立即退出的。

3. awaitTermination(long timeOut, TimeUnit unit)

当前线程阻塞,timeout 和 TimeUnit 两个参数,用于设定超时的时间及单位,当前线程阻塞,直到:

  • 等所有已提交的任务(包括正在跑的和队列中等待的)执行完;
  • 或者 等超时时间到了( timeoutTimeUnit 设定的时间);
  • 或者 线程被中断,抛出 InterruptedException 异常

然后会监测 ExecutorService 是否已经关闭,返回 true(shutdown请求后所有任务执行完毕)或 false(已超时)

4. 三种方法的区别

shutdown() 与 shutdownNow()

  • shutdown() 只是关闭了提交通道,用 submit() 是无效的;而内部该怎么跑还是怎么跑,跑完再停。
  • shutdownNow() 能立即停止线程池,正在跑的和正在等待的任务都停下了。

shutdown() 与 awaitTermination()

  • shutdown() 后,不能再提交新的任务进去;但是 awaitTermination() 后,可以继续提交。
  • awaitTermination() 是阻塞的,返回结果是线程池是否已停止(true/false);shutdown() 不阻塞。

5. 总结

  • 优雅的关闭,用 shutdown()
  • 立马关闭,并得到未执行任务列表,用 shutdownNow()
  • 优雅的关闭,并允许关闭声明后新任务能提交,用 awaitTermination()
  • 关闭功能 【从强到弱】 依次是:shuntdownNow() > shutdown() > awaitTermination()

三、如何实现线程池的优雅关闭

addShutdownHook 原理

RunTime.getRunTime().addShutdownHook() 的作用就是在 JVM 销毁前执行的最后一个线程,通过addShutdownHook 添加钩子,当系统执行完这些钩子后,JVM 才会关闭,因此我们可以在这个线程中把我们前面使用 ExecutorService 创建的线程池优雅地关闭掉。

addShutdownHook 优雅关闭线程池

在以下示例中,手动关闭 JVM 后,会触发线程池的优雅关闭:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
private static final int THREAD_CORE_SIZE = 3;
private static final int THREAD_MAX_SIZE = 10;

public static void main(String[] args) throws Exception {
System.out.println("------Start------");

// 使用 ThreadFactoryBuilder 创建自定义线程名称的 ThreadFactory
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build();
// 创建线程池,其中任务队列需要结合实际情况设置合理的容量
ThreadPoolExecutor executor = new ThreadPoolExecutor(THREAD_CORE_SIZE,
THREAD_MAX_SIZE,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingDeque<>(1024),
namedThreadFactory,
new ThreadPoolExecutor.AbortPolicy());
// 新建 1000 个任务,每个任务是打印当前线程名称
for (int i = 0; i < 1000; i++) {
executor.execute(()->{
System.out.println(Thread.currentThread().getName());
});
}
// 添加关闭线程池的钩子
Runtime.getRuntime().addShutdownHook(new Thread(()-> shutdown(executor)));

System.out.println("------done------");
}

/**
* 关闭线程池的钩子函数
* @param executorService
*/
private static void shutdown(ExecutorService executorService){
System.out.println("graceful shutdown start...");
// 第一步:使新任务无法提交
executorService.shutdown();
try {
// 第二步:等待未完成任务结束
if(!executorService.awaitTermination(5,TimeUnit.SECONDS)){
// 第三步:取消当前执行的任务
executorService.shutdownNow();
// 第四步:等待任务取消的响应
if(!executorService.awaitTermination(5,TimeUnit.SECONDS)){
LOGGER.error("Thread pool did not terminate");
}
}
} catch (InterruptedException e) {
// 第五步:出现异常后,重新取消当前执行的任务
executorService.shutdownNow();
// 设置本线程中断状态
Thread.currentThread().interrupt();
}
System.out.println("graceful shutdown end...");
}

异步调用(二)线程池的优雅关闭
http://dunkingcurry30.github.io/2022/07/08/异步调用(二)/
作者
Dunking Curry
发布于
2022年7月8日
许可协议