不少时候应用服务启动或关闭会作一些预加载(好比缓存,定时任务启动等)或收尾处理工做(好比程序失败记录等)spring
1. 首先看下Spring框架服务启动加载操做实现,直接上代码windows
继承实现接口ApplicationListener就能够实现:
import com.today.service.financereport.action.ExportReportRecordFailureAction
import com.today.service.financereport.common.ReportThreadManager
import com.today.service.financereport.dto.ExportReportFailureInput
import org.slf4j.LoggerFactory
import org.springframework.context.ApplicationListener
import org.springframework.context.event.ContextRefreshedEvent
import org.springframework.stereotype.Service
/**
* 类功能描述:容器启动监听器
*
* @author WangXueXing create at 18-11-20 上午9:35
* @version 1.0.0
*/
@Service
class ContainerStartListener extends ApplicationListener[ContextRefreshedEvent] {
private val logger = LoggerFactory.getLogger(getClass)
override def onApplicationEvent(event: ContextRefreshedEvent): Unit = {
logger.info("容器正在启动...")
Runtime.getRuntime().addShutdownHook(new Thread(() => {
logger.info("容器将要关闭,关闭前处理开始...")
//1. 设置容器关闭前还未生成报表设置为导出失败
ReportThreadManager.REPORT_THREAD_MAP.keySet().forEach { x =>
new ExportReportRecordFailureAction(ExportReportFailureInput(x, new Throwable("容器被关闭"))).execute
}
logger.info("容器将要关闭,关闭前处理完成")
}))
}
}
2. 退出服务及几种退出方法缓存
以下图:tomcat
对于强制关闭的几种状况,系统关机,操做系统会通知JVM进程关闭并等待,一旦等待超时,系统会强制停止JVM进程;kill -九、Runtime.halt()、断电、系统crash这些种方式会直接无商量停止JVM进程,JVM彻底没有执行扫尾工做的机会。所以对用应用程序而言,咱们强烈不建议使用kill -9 这种暴力方式退出。
而对于正常关闭、异常关闭的几种状况,JVM关闭前,都会调用已注册的shutdown hooks,基于这种机制,咱们能够将扫尾的工做放在shutdown hooks中,进而使咱们的应用程序安全的退出。基于平台通用性的考虑,咱们更推荐应用程序使用System.exit(0)这种方式退出JVM。安全
JVM 与 shutdown hooks 交互流程以下图所示,能够对照源码进一步的学习shutdown hooks工做原理。并发
对于tomcat类Web应用,咱们能够直接经过Runtime.addShutdownHook(Thread hook)注册自定义钩子,在钩子中实现资源的清理;而对于worker类应用,咱们能够采用以下的方式安全的退出应用。框架
信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求能够说是同样的。通俗来说,信号就是进程间的一种异步通讯机制。信号具备平台相关性,Linux平台支持的一些终止进程信号以下所示:异步
信号名称 | 用途 |
---|---|
SIGKILL | 终止进程,强制杀死进程 |
SIGTERM | 终止进程,软件终止信号 |
SIGTSTP | 中止进程,终端来的中止信号 |
SIGPROF | 终止进程,统计分布图用计时器到时 |
SIGUSR1 | 终止进程,用户定义信号1 |
SIGUSR2 | 终止进程,用户定义信号2 |
SIGINT | 终止进程,中断进程 |
SIGQUIT | 创建CORE文件终止进程,而且生成core文件 |
Windows平台存在一些差别,它的一些信号举例以下所示:ide
信号名称 | 用途 |
---|---|
SIGINT | Ctrl+C中断 |
SIGTERM | kill发出的软件终止 |
SIGBREAK | Ctrl+Break中断 |
信号选择:为了避免干扰正常信号的运做,又能模拟Java异步通知,在Linux上咱们须要先选定一种特殊的信号。经过查看信号列表上的描述,发现 SIGUSR1 和 SIGUSR2 是容许用户自定义的信号,咱们能够选择SIGUSR2,在Windows上咱们能够选择SIGINT。函数
经过这种信号机制,对应用程序JVM发送特定信号,JVM能够感知并处理该信号,进而能够接受程序退出指令。
首先看下通用的JVM安全退出的流程图:
第一步,应用进程启动的时候,初始化Signal实例,它的代码示例以下:
1
|
Signal sig = new Signal(getOSSignalType()); |
其中Signal构造函数的参数为String字符串,也就上文介绍的信号量名称。
第二步,根据操做系统的名称来获取对应的信号名称,代码以下:
1
2 3 4 5 |
private String getOSSignalType() { return System.getProperties().getProperty("os.name"). toLowerCase().startsWith("win") ? "INT" : "USR2"; } |
判断是不是windows操做系统,若是是则选择SIGINT,接收Ctrl+C中断的指令;不然选择USR2信号,接收SIGUSR2(等价于kill -12 pid)指令。
第三步,将实例化以后的SignalHandler注册到JVM的Signal,一旦JVM进程接收到kill -12 或者 Ctrl+C则回调handle接口,代码示例以下:
1
|
Signal.handle(sig, shutdownHandler); |
其中shutdownHandler实现了SignalHandler接口的handle(Signal sgin)方法,代码示例以下:
1
2 3 4 5 6 7 8 9 |
public class ShutdownHandler implements SignalHandler { /** * 处理信号 * * @param signal 信号 */ public void handle(Signal signal) { } } |
第四步,在接收到信号回调的handle接口中,初始化JVM的ShutdownHook线程,并将其注册到Runtime中,示例代码以下:
1
2 3 4 5 |
private void registerShutdownHook() { Thread t = new Thread(new ShutdownHook(), "ShutdownHook-Thread"); Runtime.getRuntime().addShutdownHook(t); } |
第五步,接收到进程退出信号后,在回调的handle接口中执行虚拟机的退出操做,示例代码以下:
1
|
Runtime.getRuntime().exit(0); |
JVM退出时,底层会自动检测用户是否注册了ShutdownHook任务,若是有,则会自动执行注册钩子的Run方法,应用只须要在ShutdownHook中执行扫尾工做便可,示例代码以下:
1
2 3 4 5 6 7 8 9 10 11 12 13 |
class ShutdownHook implements Runnable { @Override public void run() { System.out.println("ShutdownHook execute start..."); try { TimeUnit.SECONDS.sleep(10);//模拟应用进程退出前的处理操做 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("ShutdownHook execute end..."); } } |
经过以上的几个步骤,咱们能够轻松实现JVM的安全退出,另外,一般安全退出须要有超时控制机制,例如30S,若是到达超时时间仍然没有完成退出,则由停机脚本直接调用kill -9强制退出。
关闭钩子本质上是一个线程(也称为Hook线程),对于一个JVM中注册的多个关闭钩子它们将会并发执行,因此JVM并不保证它们的执行顺序;因为是并发执行的,那么极可能由于代码不当致使出现竞态条件或死锁等问题,为了不该问题,强烈建议在一个钩子中执行一系列操做。
Hook线程会延迟JVM的关闭时间,这就要求在编写钩子过程当中必需要尽量的减小Hook线程的执行时间,避免hook线程中出现耗时的计算、等待用户I/O等等操做。
为了保障应用重启过程当中异步操做的执行,避免强制退出JVM可能产生的各类问题,咱们能够采用关闭钩子、自定义信号的方式,主动的通知JVM退出,并在JVM关闭前,执行应用程序的一些扫尾工做,进一步保证应用程序能够安全的退出。