Java 优雅地退出程序
本文转载自Java 优雅地退出程序
导语
很多情况下,我们的程序需要在操作系统 后台 一直运行,这在程序代码里的实现就是用死循环 ( while (true) ) 来实现的。但是,这样会出现一个问题,就是我们想要关闭程序怎么办?如果用暴力结束进程方式,那程序的内存中若还有未输出的数据,这部分数据将会遗失。因此,我们要对程序实现 退出收尾 操作,这就需要我们完善我们的程序,实现 “优雅” 地退出。
后台进程
首先,我们需要知道什么是后台进程。众所周知,我们与服务器进行交互都需要通过终端进行实现,而在终端上执行的程序都会默认将输出打印在终端界面里,而这中方式就 交互式进程,并且当前终端只能运行一个交互进程的,所以如果我们想在一个终端里运行多个任务,我们就需要将某些进程丢到 后台 ,而这些进程不影响当前终端的交互执行,就被称为 “后台进程”。
所有的 交互式进程 都是可以转为 后台进程 的,因为进程的操作任务是一定的,只不过是它们的显示方式不同罢了,通常我们在一个终端里在任务后面加上 & 操作符就可以让交互式进程变为后台执行进程了。如:
前台进程:
git clone https://gitee.com/jiyiren/linuxfile
如果按 ctrl + c 将会结束 clone 操作。
转为 后台进程:
git clone https://gitee.com/jiyiren/linuxfile &
[1] 70235
我们可以看到此时该命令输出一个编号 70235,这个就是后台 job 的 ID,此时你按 ctrl + c 并不会结束改任务。如果要 查看 job 列表,可以使用 jobs -l, 如下:
jobs -l
[1]+ 70235 运行中 git clone https://gitee.com/jiyiren/linuxfile &
可以看到该任务在运行中,此时若想将该任务再 调到前台,可以使用 fg % jobid ( 注意百分号前后都有空格 ), 如下:
fg % 70235
git clone https://gitee.com/jiyiren/linuxfile
remote: Total 15 (delta 3), reused 0 (delta 0)
Unpacking objects: 100% (15/15), done.
此时,显示的就是正在进程的任务,如果此时按 ctrl + c 则将取消 clone 操作。
上面是基本的 Linux 前后台任务转换命令,我们可以看到我们结束进程都是将任务调到前台,然后用 ctrl + c, 来结束进程的。然而,将任务从后台调到前台的方式只能在同一个终端里操作的,如果用户在将任务掉入后台后关闭了终端窗口,那么该任务是永远无法通过 fg % jobid 调到前台了。这时如果要结束该进程怎么办?
KILL 命令
还好我们有终极杀器 – kill 命令,但 kill 命令操作的是 进程 ID 而非 job ID。也就是说 job ID 只能是同一个终端下的操作,相当于终端局域性的,而脱离了该终端后,该局域的 job ID 就不再有效。而 进程 ID 则是全局性的,任意终端都可以操作的,并且局域的 job ID 都会有与之对应的全局 进程 ID 的,因此如果关闭了那个 job ID 所在的终端,我们可以通过 kill job ID 对应的进程 ID 来结束此任务进程。
在我们平常的开发中,我们不可能一直维持着一个服务器的终端的,因此通过 ctrl + c 的方式结束 job ID 的方式对正式部署应用很不适合的,它只能适合个人的简单测试,因此 kill 命令方式才是 统一而确实有效 结束进程的方式。
假如,我们上面执行下面命令之后,就关闭掉了终端 ( 也不用管 job ID 了 ):
git clone https://gitee.com/jiyiren/linuxfile &
我们可以先通过 ps 命令来拿到我们的 进程 ID:
ps -aux | grep linuxfile | grep -v grep
jiyi 70376 0.0 0.0 116676 1536 pts/1 S 01:06 0:00 git clone https://gitee.com/jiyiren/linuxfile
jiyi 70377 5.7 0.4 174908 7952 pts/1 S 01:06 0:01 git-remote-https origin https://gitee.com/jiyiren/linuxfile
jiyi 70379 3.3 0.0 124632 1136 pts/1 Sl 01:06 0:00 git fetch-pack --stateless-rpc --stdin --lock-pack --thin https://gitee.com/jiyiren/linuxfile/
上面第一个 grep 后面就是自己要搜索的进程中包含的 关键词,这个自己根据自己的命令选择命令中的关键词,这样便于更好地过滤。第二个 grep 则是去除本身这个查找命令的意思。
我们从上面命令结果可以看到有三个进程与此任务对应,其中第二列是 进程的 ID, 我们可以用下面命令杀死该任务的所有进程:
kill -9 70376 70377 70379
这样在终端里通过 jobs -l 可以看到已经没有任务在运行了。
KILL 信号
通过上面的叙述,我们知道 kill 命令的作用。那么,上面的结束进程的命令 kill -9 的 9 是什么意思呢?实际上 kill -9 是 kill -s 9 的缩写,-s 后面接信号名称或者信号序号。而 9 代表的信号名为 SIGKILL, 也就是说 kill -9 也可以写成 kill -s SIGKILL. 此外,如果用信号名,字符的大小写是不敏感的,因此大家也可以写成 kill -s sigkill. 最后,由于所有的信号名都是以 SIG 打头的,因此,通常在我们自己写的程序中都是去掉 SIG 作为信号名的,因此,此命令还可以写成 kill -s kill. 这里我整理出 信号 9 所有相同功能的命令操作:
kill -9 [PID]
kill -s 9 [PID]
kill -s SIGKILL [PID]
kill -s sigkill [PID]
kill -s KILL [PID]
kill -s kill [PID]
大家可以把 SIGKILL 这个信号换成其他的也适用,但由于信号名称有点长,不太好记,因此,通常我们在操作命令的时候使用序号来执行 kill 命令。
那我们怎么知道有哪些信号?以及这些信号对应的序号呢?实际上 kill 命令还有一个参数 -l, 可以列出所有支持的 信号序号 以及 信号名:
kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
大家也看到了,信号太多了,这里我挑选出最长用的信号进行说明:
信号名 信号序号 含义
SIGHUP 1 终端断线
SIGINT 2 中断(同 Ctrl + C)
SIGQUIT 3 退出(同 Ctrl + \)
SIGTERM 15 正常终止
SIGKILL 9 强制终止
SIGCONT 18 继续(与STOP相反, fg/bg命令)
SIGSTOP 19 暂停(同 Ctrl + Z)
SIGUSR1 10 用户自定义信号1
SIGUSR2 12 用户自定义信号2
这里我们只取其中的 结束进程的信号 来讲:
SIGINT 2 中断(同 Ctrl + C)
SIGTERM 15 正常终止
SIGKILL 9 强制终止
其中大家经常使用的 ctrl + c 快捷键就是发送了 SIGINT(2) 信号给进程的。另外,整个信号中,最特殊的命令就是 SIGKILL(9), 它代表 无条件结束进程,也就是通常说的强制结束进程,这种方式结束进程有可能会导致进程内存中 数据丢失。而另外两个信号对于进程来说是可以选择性忽略的,但目前的绝大部分的进程都是可以通过这三个信号进行结束的。
那这三个结束命令到底有啥区别?对比如下表:
| 信号 | 快捷键 | 正常结束 | 无条件结束 | 应用场景 |
|---|---|---|---|---|
| SIGINT(2) | ctrl + c | 是 | 否 | 前台进程快捷终止 |
| SIGTERM(15) | 无 | 是 | 否 | 后台进程正常终止 |
| SIGKILL(9) | 无 | 否 | 否 | 后台进程强制终止 |
大家主要关注下各个信号的 应用场景 即可。
然而,我们的上线程序绝大部分都是后台进程在跑的,本篇内容也是讨论后台进程,因此我们主要看 后台进程的正常结束( SIGINT(2)、SIGTERM(15) ) 与 后台进程的强制结束 ( SIGKILL(9) ) 的区别。
正常与强制结束方式
本篇讨论 Java 程序的后台程序 正常 与 强制结束 方式对比。在 Java 中,强制结束代表 直接立即结束 进程中的 Main 线程和其他所有线程,这里强调 直接和立即,也就是说通过强制方式,进程不会做任何收尾工作。而 正常结束 则非立即结束进程,而是先调用程序的 收尾线程,等收尾线程结束后再结束所有线程。
这里出现了 收尾线程,实际上这个就是 Java 程序中通过 Runtime.getRuntime().addShutdownHook() 方式注册的线程就是收尾线程。为了更详细地说明正常结束与强制结束的区别我们先定义一个工作线程 JobThread:
// 工作线程,每秒钟输出一个递增的数字
public class JobThread extends Thread {
int count = 0;
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Work Thread: " + count++);
}
}
}
另外我们再定义一个收尾线程 ShudownHookThread:
// 收尾线程,没 0.5 秒输出一个递减的数字
public class ShudownHookThread extends Thread {
int count = 10;
@Override
public void run() {
while (count>0){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Shutdown Thread: "+count--);
}
}
}
现在在 Main 函数中先注册收尾线程,然后再启动工作线程:
public class Main {
public static void main(String[] args) {
Runtime.getRuntime().addShutdownHook(new ShudownHookThread());
JobThread jobThread = new JobThread();
jobThread.start();
}
}
然后打包成 Jar 包 ( 假设名字为 jvmexit-example.jar ),我们通过下面命令启动程序:
java -jar jvmexit-example.jar
0
1
2
3
.
.
我们可以看到工作线程每隔 1 秒输出一个数字,此时如果我们来通过正常和强制执行看看他们相应的输出。
正常结束 kill -2 [PID] 或者 kill -15 [PID]:

强制结束 kill -9 [PID] :

从中我们可以看出 正常结束 方式,会 先调用收尾线程,然后再结束,而 强制结束 则直接 杀死所有线程。因此,这里给出优雅结束进程说明:
- 先定义自己的 收尾线程 要完成的任务,比如:清理内存,将未完成的 IO 操作完成,删除缓存文件等等;
- Main 函数里,在主任务启动之前注册 收尾线程 即可完成收尾任务的注册;
- 使用
kill的 SIGIN(2) 和 SIGTERM(15) 两个信号进行进程结束,则 收尾线程 会被调用;
自定义 kill 信号处理
我们前面也讲过,除了信号 SIGKILL(9) 外,其他信号对于进程来说都是可忽略的。而这个忽略就是自己在自己的任务进程里实现这些信号的监听。
Java 中有提供一个接口 SignalHandler,完整名 sun.misc.SignalHandler,我们只要实现该接口,就可以在接收到信号后进行一些相应处理了。
我们定义类 SignalHandlerImp 其实现接口 SignalHandler:
public class SignalHandlerImp implements SignalHandler {
public void handle(Signal signal) {
System.out.println(signal.getName()+":"+signal.getNumber());
}
}
类内部只有一个要实现的方法 public void handle(Signal signal), 而我们在方法里仅仅是打印了信号的名称和序号。然后在 Main 函数里注册一下
public class Main {
public static void main(String[] args) {
// 注册要监听的信号
SignalHandlerImp signalHandlerImp = new SignalHandlerImp();
Signal.handle(new Signal("INT"), signalHandlerImp); // 2 : 中断(同 ctrl + c )
Signal.handle(new Signal("TERM"), signalHandlerImp); // 15 : 正常终止
Signal.handle(new Signal("USR2"), signalHandlerImp); // 12 : 用户自定义信号
JobThread jobThread = new JobThread();
jobThread.start();
}
}
主函数里我们监听了三个信号:SIGINT(2), SIGTERM(15), SIGUSR2(12), 同时我们也用到了上一节使用的工作线程 JobThread ( 注意这里没有用到上节的扫尾进程 ), 让我们来重新打包并启动任务 。
java -jar jvmexit-example.jar
0
1
2
3
.
.
执行结果是一样的,每秒输出一个数字,那我们来分别执行:
// pid 换成自己的进程 ID
kill -2 [PID]
kill -15 [PID]
kill -12 [PID]
kill -9 [PID]
得到的结果如下:

从中我们可以看出自定义的信号处理方式,正常结束的信号 ( SIGINT(2) 和 SIGTERM(15) ) 都不会结束进程,而只是执行自己自定义的方法,然而 强制结束信号 ( SIGKILL(9) ) 则不会被自定义监控,大家自己可以尝试下在 Main 函数中注册 KILL 信号,如下:
Signal.handle(new Signal("KILL"), signalHandlerImp); // 9 : 强制终止
这个在运行的时候就会报错,因此 SIGKILL(9) 信号是唯一不能够被自定义的信号。
那既然我们自己可以自定义信号,那我们通过自定义的信号来处理我们的收尾操作也是可行的。因此我们只要在 SignalHandler 接口的实现类中 handle 方法中处理自己的收尾操作就可以了。这里也整理下自定义信号处理进行收尾的说明:
- 实现
SignalHandler接口,在handle方法中实现自己的收尾操作; - Main 函数里,在主任务启动之前注册 自定义信号名 即可完成收尾任务的注册,只需要注册一个就行了;
- 使用
kill的 对应 自定义信号名 进行任务进程的结束,就可以正常收尾了。
另外,在实际操作中使用自定义信号的方式通常是直接让 工作线程 实现 SignalHandler 接口的,我们上面是为了举例,以不至于发送对应信号后进程就停止了,而实际情况下是需要我们发送信号工作线程就应该停止,因此可以将上面的工作线程修改如下:
// 工作线程,每秒钟输出一个递增的数字
public class JobThread extends Thread implements SignalHandler{
boolean isStop = fals;
int count = 0;
@Override
public void run() {
while (!isStop) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Work Thread: " + count++);
}
}
public void handle(Signal signal) {
isStop = true;
// do other something;
}
}
如上所示,加一个运行 标识,并在收到信号后进行 标识 的反赋值,这样工作线程就会自动停止,当然还可以进行其他相关操作。
两种方式对比
本文接收两种优雅 ( 而非暴力 kill -9 ) 结束进程方式:
- 采用默认信号处理机制,通过
Runtime.getRuntime().addShutdownHook(new ShudownHookThread());实现收尾进程的注册,这样在收到默认正常结束信号 ( SIGINT(2) 和 SIGTERM(15) ) 就可优雅退出; - 采用自定义信号处理机制,通过
Signal.handle(new Signal("USR2"), new SignalHandlerImp());注册 自定义信号 以及 信号处理实现类,这样使用 kill -自定义信号 ( 如: SIGUSR2(12) ) [PID] 就可以达到收尾操作在 信号处理实现类 里实现,从而也可实现优雅退出。
那这两种方式哪个更好点?或者说适应性更广泛一点?
这里我参考了 JVM 安全退出 这篇文章,它给出了 JVM 关闭的不止有 正常关闭、强制关闭 还有一种 异常关闭 如下图:

这种方式还是会调用以 Runtime.getRuntime().addShutdownHook(new ShudownHookThread()); 此方法注册的 收尾线程 的,而不会触发自定义的信号通信的。因此,还是第一种默认信号处理机制,通过 Hook 线程方式适应性更广泛。
参考
Java 优雅地退出程序的更多相关文章
- 捕获Ctrl + C中断 优雅的退出程序 golang
捕获Ctrl + C中断 优雅的退出程序 Gracefully terminate a program in Go os/signal 来捕获系统中断等信号 // Notify方法将signal发送到 ...
- JAVA优雅停机的实现
最近在项目中需要写一个数据转换引擎服务,每过5分钟同步一次数据.具体实现是启动engine server后会初始化一个ScheduledExecutorService和一个ThreadPoolExec ...
- java优雅的使用elasticsearch api
本文给出一种优雅的拼装elasticsearch查询的方式,可能会使得使用elasticsearch的方式变得优雅起来,使得代码结构很清晰易读. 建立elasticsearch连接部分请参看另一篇博客 ...
- Java优雅停机
Java的优雅停机通常通过注册JDK的ShootDownHook实现,当系统接受到退出指令后,首先标记系统处于退出状态,不再接受新的消息,然后将积压的消息处理完,最后调用资源回收接口将资源销毁,最后各 ...
- JAVA - 优雅的记录日志(log4j实战篇)
写在前面 项目开发中,记录错误日志有以下好处: 方便调试 便于发现系统运行过程中的错误 存储业务数据,便于后期分析 在java中,记录日志有很多种方式: 自己实现 自己写类,将日志数据,以io操作方式 ...
- JAVA - 优雅的记录日志(log4j实战篇) (转)
写在前面 项目开发中,记录错误日志有以下好处: 方便调试 便于发现系统运行过程中的错误 存储业务数据,便于后期分析 在java中,记录日志有很多种方式: 自己实现 自己写类,将日志数据,以io操作方式 ...
- java优雅注释原则和代码格式列举
一.java的三种注释类型 单行注释:// ...... 块注释:/* ...... */ 文档注释:/** ...... */ 二.指导原则 注释不能美化糟糕的代码,碰到糟糕的代码就重新写吧. 用代 ...
- Linux系统下如何优雅地关闭Java进程?
资料出处: http://www.sohu.com/a/329564560_700886 https://www.cnblogs.com/nuccch/p/10903162.html 前言 Linux ...
- Java 如何实现优雅停服?刨根问底
在 Java 的世界里遨游,如果能拥有一双善于发现的眼睛,有很多东西留心去看,外加耐心助力,仔细去品,往往会品出不一样的味道. 通过本次分享,能让你轻松 get 如下几点,绝对收获满满. a)如何让 ...
随机推荐
- 复制虚拟机,链接网络问题:没有找到合适的设备:没有找到可用于链接System eth0 的
http://my.oschina.net/coolfire368/blog/292742 1./etc/udev/rules.d/70-persistent-net.rules 修改也成,修改时留下 ...
- mysql错误(Incorrect key file for table)
Incorrect key file for table 'C:\Windows\TEMP\#sql578_6e2_68d.MYI'; try to repair it mysql错误:mysql需要 ...
- 配置七牛云图床 + Typora
配置七牛云图床工具 使用图床+Typora可以方便快捷的撰写图文博客 我这里以七牛云进行示例,讲解如何去配置 七牛云是属于收费图床,目前还在测试,不过对于使用量不大的我来说应该免费是足够了的,不过需要 ...
- ASP.Net Core 5.0 MVC 配置文件读取,Startup 类中ConfigureServices 方法、Configure 方法的使用
配置文件读取 1. 新建FirstController控制器 在appsettings文件内容替换成以下代码 { "Position": { "Title": ...
- 用鸿蒙开发AI应用(八)JS框架访问内核层
目录:前言JS应用开发框架原理内置模块实现ace模块开发界面程序 前言上回说到,用C++来写UI界面的开发效率不如JS+HTML来的高,但设备开发又免不了要通过内核态来操作硬件,这里我们就要先打通从J ...
- Consonant Fencity Gym - 101612C 暴力二进制枚举 Intelligence in Perpendicularia Gym - 101612I 思维
题意1: 给你一个由小写字母构成的字符串s,你可以其中某些字符变成大写字母.如果s中有字母a,你如果想把a变成大写,那s字符串中的每一个a都要变成A 最后你需要要出来所有的字符对,s[i]和s[i-1 ...
- 【uva 1152】4 Values Whose Sum is Zero(算法效率--中途相遇法+Hash或STL库)
题意:给定4个N元素几个A,B,C,D,要求分别从中选取一个元素a,b,c,d使得a+b+c+d=0.问有多少种选法.(N≤4000,D≤2^28) 解法:首先我们从最直接最暴力的方法开始思考:四重循 ...
- 牛客算法周周练20 F.紫魔法师 (二分图染色)
题意:给你一张图,对其染色,使得相连的点的颜色两两不同求,最少使用多少种颜色. 题解:首先,若\(n=1\),只需要一种.然后我们再去判断是否是二分图,对于二分图,两种颜色就够了,若不是二分图,也就是 ...
- JSR-303 实现参数校验
参考 1. 什么是JSR-303 JSR-303 是 JAVA EE 6 中的一项子规范,叫做 Bean Validation,官方参考实现是Hibernate Validator. 此实现与 Hib ...
- Nginx基础 - HTTPS安全web服务
1.HTTPS配置语法 Syntax: ssl on | off; Default: ssl off; Context: http, server Syntax: ssl_certificate fi ...