不少時候應用服務啓動或關閉會作一些預加載(好比緩存,定時任務啓動等)或收尾處理工做(好比程序失敗記錄等)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關閉前,執行應用程序的一些掃尾工做,進一步保證應用程序能夠安全的退出。