相关历史文章(阅读本文之前,您可能需要先看下之前的系列)
国内最全的Spring Boot系列之三
水满自溢「限流算法第四把法器:漏桶算法」- 第303篇
一分钟get:缓存穿透、缓存击穿、缓存雪崩 - 第304篇
布隆过滤器Bloom Filter竟然让我解决了一个大厂的问题 - 第305篇
100G的文件如何读取 - 第306篇
100G的文件如何读取续集 - 第307篇
师傅:徒儿,在接下来的章节中,我们一起来学习下Spring Boot如何优雅停机。
悟纤:师傅,棒棒的。
师傅:来,你来说下何为优雅?
悟纤:指人行为举止优美,自然且高雅。
师傅:欧侯,徒儿,你这个解释,我竟无言以对。你能审下题不?
悟纤:师傅,你也没有什么定语呐,我就以为是解释下优雅的意思了。我哪知道你要的是优雅的停止服务的意思呢。
师傅:哎,大伙说下,我这容易嘛。那你说说看。
悟纤:这个… 这个….
师傅:我看你就是过过嘴瘾呢。还得为师和你讲一下。
悟纤:徒儿虚心求教。
一、何为优雅停机?
1.1 定义
什么叫优雅停机?简单说就是,在对应用进程发送停止指令之后,能保证正在执行的业务操作不受影响。
应用接收到停止指令之后的步骤应该是:停止接收访问请求,等待已经接收到的请求处理完成,并能成功返回,这时才真正停止应用。
1.2 JVM Hook
Runtime.getRuntime().addShutdownHook(Thread hook):这个方法的意思就是在jvm中增加一个关闭的钩子,当jvm关闭的时候,会执行系统中已经设置的所有通过方法addShutdownHook添加的钩子,当系统执行完这些钩子后,jvm才会关闭。所以这些钩子可以在jvm关闭的时候进行内存清理、对象销毁等操作。
注册一个JVM关闭的钩子,这个钩子可以在以下几种场景被调用:
(1)程序正常退出;
(2)使用System.exit();
(3)终端使用Ctrl+C触发的中断;
(4)系统关闭;
(5)使用kill pid命令干掉进程;
1.3 JVM Hook的小例子1
编写一个代码继承Thread,并且使用Runtime.getRuntime().addShutdownHook(this) 将此线程添加到JVM的shutdown钩子中:
public class ShutdownHook extends Thread {
@Override
public void run() {
System.out.println("Shut down signal received.");
//do something.
System.out.println("Shut down complete.");
}
public ShutdownHook() {
Runtime.getRuntime().addShutdownHook(this);
}
}
写个main来测试下:
public class TestMain {
private ShutdownHook shutdownHook;
public TestMain() {
this.shutdownHook = new ShutdownHook();
}
public static void main(String[] args) {
TestMain app = new TestMain();
System.out.println("start of main()");
//do something.
System.out.println("End of main()");
}
}
使用run main运行下,查看控制台:
start of main()
End of main()
Shut down signal received.
Shut down complete.
这种情况就是第一种情况(1)程序正常退出:执行完成main方法之后,程序就结束退出了,那么就会执行hook,也就是会执行线程的run()方法。
1.4 JVM Hook的小例子2
那么如何来模拟下使用kill pid的方式呐,很简单只要稍微修改下我们的代码。
我们一个核心的思路就是让main方法不要结束。我们可以在ShutdownHook定义一个是否接收到了shutdown的信号变量isReceivedSignal,然后在main方法执行有一个方法,通过此参数来循环执行即可,看代码:
ShutdownHook:
public class ShutdownHook extends Thread {
public boolean isReceivedSignal = false;
@Override
public void run() {
System.out.println("Shut down signal received.");
this.isReceivedSignal = true;
//do something.
System.out.println("Shut down complete.");
}
public ShutdownHook() {
Runtime.getRuntime().addShutdownHook(this);
}
}
注意:这里多了一个变量isReceivedSignal ,在接收到shutdown的信号的时候,会执行run方法,然后执行完成之后,将变量置为true。
TestMain:
public class TestMain {
private ShutdownHook shutdownHook;
public TestMain() {
this.shutdownHook = new ShutdownHook();
}
public void execute() {
while(!shutdownHook.isReceivedSignal) {
System.out.println("I am sleep");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("I am not sleep");
}
System.out.println("end execute");
}
public static void main(String[] args) {
TestMain app = new TestMain();
System.out.println("start of main()");
//do something.
app.execute();
System.out.println("End of main()");
}
}
说明: 在testmain类中添加了一个execute方法,此方法就是根据ShutdownHook中的变量来进行判断是否进行while循环的。
使用run main在运行下,然后使用kill pid的方式进行关闭(我是mac电脑,终端就可以使用kill了)。
对于kill pid 实际上等同于kill -15 pid(kill -s 15 pid),使用kill -l可以查看到所有的信号意思:
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL
5) SIGTRAP 6) SIGABRT 7) SIGEMT 8) SIGFPE
9) SIGKILL 10) SIGBUS 11) SIGSEGV 12) SIGSYS
13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGURG
17) SIGSTOP 18) SIGTSTP 19) SIGCONT 20) SIGCHLD
21) SIGTTIN 22) SIGTTOU 23) SIGIO 24) SIGXCPU
25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH
29) SIGINFO 30) SIGUSR1 31) SIGUSR2
kill pid和kill -s 15 pid含义一样,表示发送一个SIGTERM的信号给对应的程序。程序收到该信号后,将会发生以下事情:
(1)程序立刻停止;
(2)程序释放相应资源后立刻停止;
(3)程序可能仍然继续运行;
大部分程序在接收到SIGTERM信号后,会先释放自己的资源,然后再停止。但也有一些程序在收到信号后,做一些其他事情,并且这些事情是可以配置的。也就是说,SIGTERM多半是会被阻塞,忽略的。
对于kill -9 pid是我们常见的,意思是SIGKILL,表示强制、尽量终止一个进程。
来我们运行下kill pid(pid可以使用jps命令进行查看)。
查看进程ID:
$ jps
96512 Jps
96510 TestMain
关闭进程TestMain:kill 96510。
查看控制台信息:
I am sleep
I am not sleep
I am sleep
Shut down signal received.
Shut down complete.
观察控制台的打印,我们不难发现:
(1)execute并未执行完;
(2)main方法也并未执行完;
所以这个kill之后,只能发出一个对于添加到了addShutdownHook的线程,对于主线程等同于就是直接结束了吗??
那么我们是否可以等待主线程执行完毕呐,答案是可以的,主要使用线程的join方法即可。
1.5 JVM Hook的小例子3
我们先猜想:对于主线程是否需是我们的ShuwdownHook的run方法执行太快了,然后JVM就认为全部执行完成了,就释放了资源,直接关闭了进程了呐。
猜想归猜想,可以简单验证下,在ShutdownHook的run方法,当接收到关闭信号的时候,先休眠个1秒:
@Override
public void run() {
System.out.println("Shut down signal received.");
this.isReceivedSignal = true;
//do something.
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Shut down complete.");
}
运行看一下效果:
I am sleep
I am not sleep
I am sleep //进入while循环了
Shut down signal received. //接收到关闭信号,后sleep了.
I am not sleep //main thread继续执行
end execute //execute执行完毕
End of main() //跳出execute方法,回到main方法.
Shut down complete. //run方法执行完毕。
这种方式main是可以执行完毕了,但是我们很多时候,并不知道main方法还需要执行多久才能执行完毕,那么这个时候thread.join方法就派上用场了。
1.6 JVM Hook的小例子6
我们这个例子改造的核心思路就是,我们需要将主线程作为参数传到ShutdownHook类中,然后在ShutdownHook的run方法里执行Thread.join等待主线程执行完毕。
ShutdownHook:
public class ShutdownHook extends Thread {
public boolean isReceivedSignal = false;
private Thread mainThread;
@Override
public void run() {
System.out.println("Shut down signal received.");
this.isReceivedSignal = true;
//do something.
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
try {
mainThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Shut down complete.");
}
public ShutdownHook(Thread mainThread) {
this.mainThread = mainThread;
Runtime.getRuntime().addShutdownHook(this);
}
}
说明:构建方法接收一个mainThread,在run方法使用join等待主线程执行完毕。
TestMain:
public class TestMain {
private ShutdownHook shutdownHook;
public TestMain() {
this.shutdownHook = new ShutdownHook(Thread.currentThread());
}
public void execute() {
while(!shutdownHook.isReceivedSignal) {
System.out.println("I am sleep");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("I am not sleep");
}
System.out.println("end execute");
}
public static void main(String[] args) {
TestMain app = new TestMain();
System.out.println("start of main()");
//do something.
app.execute();
System.out.println("End of main()");
}
}
说明:TestMain没什么特殊之处,就是把当前自己所属的线程传给了ShutdownHook。
来run一下吧,使用kill pid关闭:
I am sleep
I am not sleep
I am sleep
Shut down signal received.
I am not sleep
end execute
End of main()
Shut down complete.
二、悟纤小结
师傅:我们今天就先介绍到这里,下节我们来说说Spring Boot的优雅停机方式。
悟纤:师傅棒棒的,剩下的就让徒儿来给师傅总结下。
(1)优雅停机?简单说就是,在对应用进程发送停止指令之后,能保证正在执行的业务操作不受影响。
(2)Runtime.getRuntime().addShutdownHook(Thread hook):这个方法的意思就是在jvm中增加一个关闭的钩子,当jvm关闭的时候,会执行系统中已经设置的所有通过方法addShutdownHook添加的钩子,当系统执行完这些钩子后,jvm才会关闭。所以这些钩子可以在jvm关闭的时候进行内存清理、对象销毁等操作。
(3)对于kill指令-s 9和15是有区别的:-s 9:强制关闭进程;-s 15:发出终止信号。
我就是我,是颜色不一样的烟火。
我就是我,是与众不同的小苹果。
学院中有Spring Boot相关的课程:
à悟空学院:https://t.cn/Rg3fKJD
SpringBoot视频:http://t.cn/A6ZagYTi
Spring Cloud视频:http://t.cn/A6ZagxSR
SpringBoot Shiro视频:http://t.cn/A6Zag7IV
SpringBoot交流平台:https://t.cn/R3QDhU0
SpringData和JPA视频:http://t.cn/A6Zad1OH
SpringSecurity5.0视频:http://t.cn/A6ZadMBe
Sharding-JDBC分库分表实战:http://t.cn/A6ZarrqS
分布式事务解决方案「手写代码」:http://t.cn/A6ZaBnIr
JVM内存模型和性能调优:http://t.cn/A6wWMVqG