本文转载自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 -99 是什么意思呢?实际上 kill -9kill -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 函数里,在主任务启动之前注册 收尾线程 即可完成收尾任务的注册;
  • 使用 killSIGIN(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 ) 结束进程方式:

  1. 采用默认信号处理机制,通过 Runtime.getRuntime().addShutdownHook(new ShudownHookThread()); 实现收尾进程的注册,这样在收到默认正常结束信号 ( SIGINT(2)SIGTERM(15) ) 就可优雅退出;
  2. 采用自定义信号处理机制,通过 Signal.handle(new Signal("USR2"), new SignalHandlerImp()); 注册 自定义信号 以及 信号处理实现类,这样使用 kill -自定义信号 ( 如: SIGUSR2(12) ) [PID] 就可以达到收尾操作在 信号处理实现类 里实现,从而也可实现优雅退出。

那这两种方式哪个更好点?或者说适应性更广泛一点?

这里我参考了 JVM 安全退出 这篇文章,它给出了 JVM 关闭的不止有 正常关闭强制关闭 还有一种 异常关闭 如下图:

这种方式还是会调用以 Runtime.getRuntime().addShutdownHook(new ShudownHookThread()); 此方法注册的 收尾线程 的,而不会触发自定义的信号通信的。因此,还是第一种默认信号处理机制,通过 Hook 线程方式适应性更广泛。

参考

Java 优雅地退出程序的更多相关文章

  1. 捕获Ctrl + C中断 优雅的退出程序 golang

    捕获Ctrl + C中断 优雅的退出程序 Gracefully terminate a program in Go os/signal 来捕获系统中断等信号 // Notify方法将signal发送到 ...

  2. JAVA优雅停机的实现

    最近在项目中需要写一个数据转换引擎服务,每过5分钟同步一次数据.具体实现是启动engine server后会初始化一个ScheduledExecutorService和一个ThreadPoolExec ...

  3. java优雅的使用elasticsearch api

    本文给出一种优雅的拼装elasticsearch查询的方式,可能会使得使用elasticsearch的方式变得优雅起来,使得代码结构很清晰易读. 建立elasticsearch连接部分请参看另一篇博客 ...

  4. Java优雅停机

    Java的优雅停机通常通过注册JDK的ShootDownHook实现,当系统接受到退出指令后,首先标记系统处于退出状态,不再接受新的消息,然后将积压的消息处理完,最后调用资源回收接口将资源销毁,最后各 ...

  5. JAVA - 优雅的记录日志(log4j实战篇)

    写在前面 项目开发中,记录错误日志有以下好处: 方便调试 便于发现系统运行过程中的错误 存储业务数据,便于后期分析 在java中,记录日志有很多种方式: 自己实现 自己写类,将日志数据,以io操作方式 ...

  6. JAVA - 优雅的记录日志(log4j实战篇) (转)

    写在前面 项目开发中,记录错误日志有以下好处: 方便调试 便于发现系统运行过程中的错误 存储业务数据,便于后期分析 在java中,记录日志有很多种方式: 自己实现 自己写类,将日志数据,以io操作方式 ...

  7. java优雅注释原则和代码格式列举

    一.java的三种注释类型 单行注释:// ...... 块注释:/* ...... */ 文档注释:/** ...... */ 二.指导原则 注释不能美化糟糕的代码,碰到糟糕的代码就重新写吧. 用代 ...

  8. Linux系统下如何优雅地关闭Java进程?

    资料出处: http://www.sohu.com/a/329564560_700886 https://www.cnblogs.com/nuccch/p/10903162.html 前言 Linux ...

  9. Java 如何实现优雅停服?刨根问底

    在 Java 的世界里遨游,如果能拥有一双善于发现的眼睛,有很多东西留心去看,外加耐心助力,仔细去品,往往会品出不一样的味道. 通过本次分享,能让你轻松 get 如下几点,绝对收获满满. a)如何让 ...

随机推荐

  1. Spark——DataFrames,RDD,DataSets、广播变量与累加器

    Spark--DataFrames,RDD,DataSets 一.弹性数据集(RDD) 创建RDD 1.1RDD的宽依赖和窄依赖 二.DataFrames 三.DataSets 四.什么时候使用Dat ...

  2. Java中的transient关键字,使用小结

    transient关键字的介绍: 一个对象只要实现了Serilizable接口,这个对象就可以被序列化,Java的这种序列化模式为开发者提供了很多便利,可以不必关系具体序列化的过程,只要这个类实现了S ...

  3. python--函数、参数、名称空间与作用域、匿名函数、内置函数、闭包

    python函数 函数定义 def welcome(): print('hello world!!') welcome() #函数调用 ...运行结果 hello world!! 函数定义和编写原则: ...

  4. linux中在某个目录下多个文件中搜索关键字

    有四种方法: find 文件目录 -name '*.*' -exec grep 'xxx' {} + -n 或是 find 文件目录 -name '*.*' | xargs grep 'xxx' -n ...

  5. Codeforces Global Round 8 C. Even Picture(构造)

    题目链接:https://codeforces.com/contest/1368/problem/C 题意 构造一个只含有灰.白块的网格,要求: 所有灰块为一个连通块 每个灰块与偶数个灰块相邻 恰有 ...

  6. 【noi 2.6_9280】&【bzoj 1089】严格n元树(DP+高精度+重载运算符)

    题意:定义一棵树的所有非叶节点都恰好有n个儿子为严格n元树.问深度为d的严格n元树数目. 解法:f[i]表示深度为<=i的严格n元树数目.f[i]-f[i-1]表示深度为i的严格n元树数目.f[ ...

  7. Codeforces Round #582 (Div. 3) F. Unstable String Sort

    传送门 题意: 你需要输出一个长度为n的字符序列(由小写字母组成),且这个字符串中至少包含k个不同的字符.另外题目还有要求:给你两个长度为p和q的序列,设字符序列存在s中 那么就会有s[Pi]< ...

  8. ASP.Net Core 5.0 MVC中AOP思想的体现(五种过滤器)并结合项目案例说明过滤器的用法

    执行顺序 使用方法,首先实现各自的接口,override里面的方法, 然后在startup 类的 ConfigureServices 方法,注册它们. 下面我将代码贴出来,照着模仿就可以了 IActi ...

  9. python之字符串split和rsplit的方法

    1.描述 split()方法通过指定分隔符对字符串进行切片,如果参数num有指定值,则分隔num+1个子字符串,默认分隔符为所有空字符,包括空格.换行(\n).制表符(\t)等 rstrip()方法通 ...

  10. springmvc拦截器实现登录验证

    首先创建一个实体类: Customer: 1 package com.petcare.pojo.base; 2 3 import java.sql.Date; 4 import java.sql.Ti ...