精品專欄
優雅停機? 這個名詞我是服的,如果拋開專業不談,多好的名詞啊!
其實優雅停機,就是在要關閉服務之前,不是立馬全部關停,而是做好一些善後操作,比如:關閉執行緒、釋放連線資源等。
再比如,就是不會讓呼叫方的請求處理了一增,一下就中斷了。而處理完本次後,再停止服務。
Java語言中,我們可以透過Runtime.getRuntime().addShutdownHook()方法來註冊鉤子,以保證程式平滑退出。(其他語言也類似)
來個慄子:
public class ShutdownGracefulTest {
/**
* 使用執行緒池處理任務
*/
public static ExecutorService executorService = Executors.newCachedThreadPool();
public static void main(String[] args) {
//假設有5個執行緒需要執行任務
for(int i = 0; i < 5; i++){
final int id = i;
Thread taski = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(System.currentTimeMillis() + " : thread_" + id + " start...");
try {
TimeUnit.SECONDS.sleep(id);
}
catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis() + " : thread_" + id + " finish!");
}
});
taski.setDaemon(true);
executorService.submit(taski);
}
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
System.out.println(System.currentTimeMillis() + " : " + Thread.currentThread().getName() + " No1 shutdown hooking...");
boolean shutdown = true;
try {
executorService.shutdown();
System.out.println(System.currentTimeMillis() + " : " + Thread.currentThread().getName() + " shutdown signal got, wait threadPool finish.");
executorService.awaitTermination(1500, TimeUnit.SECONDS);
boolean done = false;
System.out.println(System.currentTimeMillis() + " : " + Thread.currentThread().getName() + " all thread's done.");
}
catch (InterruptedException e) {
e.printStackTrace();
// 嘗試再次關閉
if(!executorService.isTerminated()) {
executorService.shutdownNow();
}
}
System.out.println(System.currentTimeMillis() + " : " + Thread.currentThread().getName() + " No1 shutdown done...");
}
}));
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println(System.currentTimeMillis() + " : " + Thread.currentThread().getName() + " No2 shutdown hooking...");
Thread.sleep(1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis() + " : " + Thread.currentThread().getName() + " No2 shutdown done...");
}
}));
System.out.println("main method exit...");
System.exit(0);
}
}
執行結果如下:
很明顯,確實是優雅了,雖然最後收到了一關閉訊號,但是仍然保證了任務的處理完成。很棒吧!
那麼,在實際應用中是如何體現優雅停機呢?
kill -15 pid
透過該命令傳送一個關閉訊號給到jvm, 然後就開始執行 Shutdown Hook 了,你可以做很多:
- 關閉 socket 連結
- 清理臨時檔案
- 傳送訊息通知給訂閱方,告知自己下線
- 將自己將要被銷毀的訊息通知給子行程
- 各種資源的釋放 …
而在平時工作中,我們不乏看到很多運維同學,是這麼乾的:
kill -9 pid
如果這麼乾的話,jvm也無法了,kill -9 相當於一次系統宕機,系統斷電。這會給應用殺了個措手不及,沒有留給應用任何反應的機會。
所以,無論如何是優雅不起來了。
要優雅,是程式碼和運維的結合!
其中,執行緒池的關閉方式為:
executorService.shutdown();
executorService.awaitTermination(1500, TimeUnit.SECONDS);
ThreadPoolExecutor 在 shutdown 之後會變成 SHUTDOWN 狀態,無法接受新的任務,隨後等待正在執行的任務執行完成。意味著,shutdown 只是發出一個命令,至於有沒有關閉還是得看執行緒自己。
ThreadPoolExecutor 對於 shutdownNow 的處理則不太一樣,方法執行之後變成 STOP 狀態,並對執行中的執行緒呼叫 Thread.interrupt() 方法(但如果執行緒未處理中斷,則不會有任何事發生),所以並不代表“立刻關閉”。
- shutdown() :啟動順序關閉,其中執行先前提交的任務,但不接受新任務。如果已經關閉,則呼叫沒有附加效果。此方法不等待先前提交的任務完成執行。
- shutdownNow():嘗試停止所有正在執行的任務,停止等待任務的處理,並傳回正在等待執行的任務的串列。當從此方法傳回時,這些任務將從任務佇列中耗盡(刪除)。此方法不等待主動執行的任務終止。
- executor.awaitTermination(this.awaitTerminationSeconds, TimeUnit.SECONDS)); 控制等待的時間,防止任務無限期的執行(前面已經強調過了,即使是 shutdownNow 也不能保證執行緒一定停止執行)。
註意:
- 虛擬機器會對多個shutdownhook以未知的順序呼叫,都執行完後再退出。
- 如果接收到 kill -15 pid 命令時,執行阻塞操作,可以做到等待任務執行完成之後再關閉 JVM。同時,也解釋了一些應用執行 kill -15 pid 無法退出的問題,如:中斷被阻塞了,或者hook運行了死迴圈程式碼。
實現原理:
Runtime.getRuntime().addShutdownHook(hook); // 新增鉤子,開啟優雅之路
// 具體流程如下:
/**
* Registers a new virtual-machine shutdown hook.
*
* @param hook
* An initialized but unstarted {@link Thread} object
*
* @throws IllegalArgumentException
* If the specified hook has already been registered,
* or if it can be determined that the hook is already running or
* has already been run
*
* @throws IllegalStateException
* If the virtual machine is already in the process
* of shutting down
*
* @throws SecurityException
* If a security manager is present and it denies
* {@link RuntimePermission}("shutdownHooks")
*
* @see #removeShutdownHook
* @see #halt(int)
* @see #exit(int)
* @since 1.3
*/
public void addShutdownHook(Thread hook) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("shutdownHooks"));
}
// 新增到 application 中
ApplicationShutdownHooks.add(hook);
}
// java.lang.ApplicationShutdownHooks.add(hook);
static synchronized void add(Thread hook) {
if(hooks == null)
throw new IllegalStateException("Shutdown in progress");
if (hook.isAlive())
throw new IllegalArgumentException("Hook already running");
if (hooks.containsKey(hook))
throw new IllegalArgumentException("Hook previously registered");
// hooks 以map型別儲存, k->k 形式儲存,保證每一個鉤子都是獨立的
hooks.put(hook, hook);
}
// java.lang.ApplicationShutdownHooks 會先註冊一個靜態塊,新增一個任務到 Shutdown 中
/* The set of registered hooks */
private static IdentityHashMap<Thread, Thread> hooks;
static {
try {
Shutdown.add(1 /* shutdown hook invocation order */,
false /* not registered if shutdown in progress */,
new Runnable() {
public void run() {
// 即當該任務被呼叫時,呼叫自身的執行方法,使所有註冊的 hook 執行起來
runHooks();
}
}
);
hooks = new IdentityHashMap<>();
} catch (IllegalStateException e) {
// application shutdown hooks cannot be added if
// shutdown is in progress.
hooks = null;
}
}
// runHooks 執行所有鉤子執行緒,進行非同步呼叫
/* Iterates over all application hooks creating a new thread for each
* to run in. Hooks are run concurrently and this method waits for
* them to finish.
*/
static void runHooks() {
Collection<Thread> threads;
synchronized(ApplicationShutdownHooks.class) {
threads = hooks.keySet();
hooks = null;
}
for (Thread hook : threads) {
hook.start();
}
for (Thread hook : threads) {
try {
// 阻塞等待所有完成
hook.join();
} catch (InterruptedException x) { }
}
}
到現在為止,我們已經知道關閉鉤子是如何執行的,但是,還不是知道,該鉤子是何時觸發?
// java.lang.Shutdown.add() 該方法會jvm主動呼叫,從而觸發 後續鉤子執行
/* Invoked by the JNI DestroyJavaVM procedure when the last non-daemon
* thread has finished. Unlike the exit method, this method does not
* actually halt the VM.
*/
static void shutdown() {
synchronized (lock) {
switch (state) {
case RUNNING: /* Initiate shutdown */
state = HOOKS;
break;
case HOOKS: /* Stall and then return */
case FINALIZERS:
break;
}
}
synchronized (Shutdown.class) {
// 執行序列
sequence();
}
}
// 而 sequence() 則會呼叫 runHooks(), 呼叫自定義的鉤子任務
private static void sequence() {
synchronized (lock) {
/* Guard against the possibility of a daemon thread invoking exit
* after DestroyJavaVM initiates the shutdown sequence
*/
if (state != HOOKS) return;
}
runHooks();
boolean rfoe;
synchronized (lock) {
state = FINALIZERS;
rfoe = runFinalizersOnExit;
}
if (rfoe) runAllFinalizers();
}
// 執行鉤子,此處最多允許註冊 10 個鉤子,且進行同步呼叫,當然這是最頂級的鉤子,鉤子下還可以新增鉤子,可以任意新增n個
private static void runHooks() {
for (int i=0; i < MAX_SYSTEM_HOOKS; i++) {
try {
Runnable hook;
synchronized (lock) {
// acquire the lock to make sure the hook registered during
// shutdown is visible here.
currentRunningHook = i;
hook = hooks[i];
}
// 同步呼叫註冊的hook, 即 前面看到 ApplicationShutdownHooks.runHooks()
if (hook != null) hook.run();
} catch(Throwable t) {
if (t instanceof ThreadDeath) {
ThreadDeath td = (ThreadDeath)t;
throw td;
}
}
}
}
如此,整個關閉流程完美了。
簡化為:
- 註冊流程(應用主動呼叫):Runtime.addShutdownHook -> ApplicationShutdownHooks.add()/static -> java.lang.Shutdown.add()/shutdown()
- 執行流程(jvm自動呼叫):java.lang.Shutdown.shutdown()->sequence()->runHooks() -> ApplicationShutdownHooks.runHooks() -> hooks 最終
出處:https://www.cnblogs.com/yougewe/p/9881874.html
多執行緒:為什麼在while迴圈中加入System.out.println,執行緒可以停止
END
>>>>>> 加群交流技術 <<<<<<