⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 网络 「网络」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

优雅停机?这个名词我是服的,如果抛开专业不谈,多好的名词啊!

其实优雅停机,就是在要关闭服务之前,不是立马全部关停,而是做好一些善后操作,比如:关闭线程、释放连接资源等。

再比如,就是不会让调用方的请求处理了一增,一下就中断了。而处理完本次后,再停止服务。

Java语言中,我们可以通过Runtime.getRuntime().addShutdownHook()方法来注册钩子,以保证程序平滑退出。(其他语言也类似)

来个栗子:

public class ShutdownGraceFullTest {
/**
* 使用线程池处理任务
*/
public static ExecutorService executorService = Executors.newCachedThreadPool();

public static void main(String[] args) {
//假设有5个线程需要执行任务
for (int i = 0; i < 5; i++) {
final int id = i;

Thread taski = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(System.currentTimeMillis() +
" : thread_" + id + " start...");

try {
TimeUnit.SECONDS.sleep(id);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println(System.currentTimeMillis() +
" : thread_" + id + " finish!");
}
});
taski.setDaemon(true);
executorService.submit(taski);
}

// 添加一个钩子处理未完任务
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
System.out.println(System.currentTimeMillis() + " : " +
Thread.currentThread().getName() +
" No1 shutdown hooking...");

boolean shutdown = true;

try {
executorService.shutdown();

System.out.println(System.currentTimeMillis() + " : " +
Thread.currentThread().getName() +
" shutdown signal got, wait threadPool finish.");
executorService.awaitTermination(1500, TimeUnit.SECONDS);

System.out.println(System.currentTimeMillis() + " : " +
Thread.currentThread().getName() +
" all thread's done.");
}
catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println(System.currentTimeMillis() + " : " +
Thread.currentThread().getName() +
" No1 shutdown done...");
}
}));

// 多个关闭钩子并发执行
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println(System.currentTimeMillis() + " : " +
Thread.currentThread().getName() +
" No2 shutdown hooking...");

Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println(System.currentTimeMillis() + " : " +
Thread.currentThread().getName() +
" No2 shutdown done...");
}
}));

System.out.println("main method exit...");

// 故意调用jvm退出命令,发送关闭信号,否则正常情况下 jvm 会等待最后一个非守护线程关闭才会退出
System.exit(0);
}
}

运行结果如下:

图片

很明显,确实是优雅了,虽然最后收到了一关闭信号,但是仍然保证了任务的处理完成。很棒吧!

那么,在实际应用中是如何体现优雅停机呢?

kill -15 pid

通过该命令发送一个关闭信号给到jvm, 然后就开始执行 Shutdown Hook 了,你可以做很多:

1、 关闭 socket 链接

2、 清理临时文件

3、 发送消息通知给订阅方,告知自己下线

4、 将自己将要被销毁的消息通知给子进程

5、 各种资源的释放

...

而在平时工作中,我们不乏看到很多运维同学,是这么干的:

kill -9 pid

如果这么干的话,jvm也无法了,kill -9 相当于一次系统宕机,系统断电。这会给应用杀了个措手不及,没有留给应用任何反应的机会。

所以,无论如何是优雅不起来了。

要优雅,是代码

其中,线程池的关闭方式为:

executorService.shutdown();executorService.awaitTermination(1500, TimeUnit.SECONDS);

ThreadPoolExecutor 在 shutdown 之后会变成 SHUTDOWN 状态,无法接受新的任务,随后等待正在执行的任务执行完成。意味着,shutdown 只是发出一个命令,至于有没有关闭还是得看线程自己。

ThreadPoolExecutor 对于 shutdownNow 的处理则不太一样,方法执行之后变成 STOP 状态,并对执行中的线程调用 Thread.interrupt() 方法(但如果线程未处理中断,则不会有任何事发生),所以并不代表“立刻关闭”。

shutdown() :启动顺序关闭,其中执行先前提交的任务,但不接受新任务。如果已经关闭,则调用没有附加效果。此方法不等待先前提交的任务完成执行。

shutdownNow():尝试停止所有正在执行的任务,停止等待任务的处理,并返回正在等待执行的任务的列表。当从此方法返回时,这些任务将从任务队列中耗尽(删除)。此方法不等待主动执行的任务终止。

executor.awaitTermination(this.awaitTerminationSeconds, TimeUnit.SECONDS)); 控制等待的时间,防止任务无限期的运行(前面已经强调过了,即使是 shutdownNow 也不能保证线程一定停止运行)。

注意:

虚拟机会对多个shutdownhook以未知的顺序调用,都执行完后再退出。

如果接收到 kill -15 pid 命令时,执行阻塞操作,可以做到等待任务执行完成之后再关闭 JVM。同时,也解释了一些应用执行 kill -15 pid 无法退出的问题,如:中断被阻塞了,或者hook运行了死循环代码。

文章目录