Java程式經常會遇到程式掛掉的情況,一些狀態沒有正確的儲存下來,這時候就需要在JVM關掉的時候執行一些清理現場的程式碼。JAVA中的ShutdownHook提供了比較好的方案。而在SOFAJRaft-example模組的CounterServer-main方法中就使用了shutdownHook實現優雅停機。
@Author:Akai-yuan
@更新時間:2023/1/25
1.觸發場景與失效場景
JDK提供了Java.Runtime.addShutdownHook(Thread hook)方法,可以註冊一個JVM關閉的鉤子這個鉤子可以在以下幾種場景中被呼叫:
- 程式正常退出
- 執行了System.exit()方法
- 終端使用Ctrl+C觸發的中斷
- 系統關閉
- OutOfMemory當機
- 使用Kill pid命令幹掉程式(使用 **kill -9 pid **是不會被呼叫的)
以下幾種情況中是無法被呼叫的:
- 透過kill -9命令殺死程式——所以kill -9一定要慎用;
- 程式中執行了Runtime.getRuntime().halt()方法;
- 作業系統突然崩潰,或機器掉電(用電裝置因斷電、失電、或電的質量達不到要求而不能正常工作)。
2.addShutdownHook方法簡述
Runtime.getRuntime().addShutdownHook(shutdownHook);
該方法指,在JVM中增加一個關閉的鉤子,當JVM關閉的時候,會執行系統中已經設定的所有透過方法addShutdownHook新增的鉤子,當系統執行完這些鉤子後,JVM才會關閉。所以這些鉤子可以在JVM關閉的時候進行記憶體清理、物件銷燬、關閉連線等操作。
3.SOFAJRaft中鉤子函式的實現
透過反射獲取到grpcServer例項的shutdown方法和awaitTerminationLimit方法,並新增到鉤子函式當中
public static void blockUntilShutdown() {
if (rpcServer == null) {
return;
}
//當RpcFactoryHelper中維護的工廠型別是GrpcRaftRpcFactory時進入if條件內部
if ("com.alipay.sofa.jraft.rpc.impl.GrpcRaftRpcFactory".equals(RpcFactoryHelper.rpcFactory().getClass()
.getName())) {
try {
//反射獲取grpcServer中維護的(io.grpc包下的)server例項
Method getServer = rpcServer.getClass().getMethod("getServer");
Object grpcServer = getServer.invoke(rpcServer);
//反射獲取server例項的shutdown方法和awaitTerminationLimit方法
Method shutdown = grpcServer.getClass().getMethod("shutdown");
Method awaitTerminationLimit = grpcServer.getClass().getMethod("awaitTermination", long.class,
TimeUnit.class);
//新增一個shutdownHook執行緒執行方法
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
try {
shutdown.invoke(grpcServer);
awaitTerminationLimit.invoke(grpcServer, 30, TimeUnit.SECONDS);
} catch (Exception e) {
// Use stderr here since the logger may have been reset by its JVM shutdown hook.
e.printStackTrace(System.err);
}
}
});
//執行awaitTermination方法
Method awaitTermination = grpcServer.getClass().getMethod("awaitTermination");
awaitTermination.invoke(grpcServer);
} catch (Exception e) {
LOG.error("Failed to block grpc server", e);
}
}
}
4.grpc中的shutdown方法
GrpcServer下的shutdown方法與本文的鉤子函式無關,此處再對比分析一下GrpcServer的shutdown方法。
public void shutdown() {
//CAS
//當且僅當期待值為true時(與當前AtomicBoolean型別的started一致),設定為false關閉
if (!this.started.compareAndSet(true, false)) {
return;
}
ExecutorServiceHelper.shutdownAndAwaitTermination(this.defaultExecutor);
GrpcServerHelper.shutdownAndAwaitTermination(this.server);
}
ExecutorServiceHelper#shutdownAndAwaitTermination:
我們可以發現實際上就是在執行ExecutorService 中 的shutdown()、shutdownNow()、awaitTermination() 方法,那麼我們來區別以下這幾個方法
public static boolean shutdownAndAwaitTermination(final ExecutorService pool, final long timeoutMillis) {
if (pool == null) {
return true;
}
// 禁止提交新任務
pool.shutdown();
final TimeUnit unit = TimeUnit.MILLISECONDS;
final long phaseOne = timeoutMillis / 5;
try {
// 等待一段時間以終止現有任務
if (pool.awaitTermination(phaseOne, unit)) {
return true;
}
pool.shutdownNow();
// 等待一段時間,等待任務響應被取消
if (pool.awaitTermination(timeoutMillis - phaseOne, unit)) {
return true;
}
LOG.warn("Fail to shutdown pool: {}.", pool);
} catch (final InterruptedException e) {
// (Re-)cancel if current thread also interrupted
pool.shutdownNow();
// preserve interrupt status
Thread.currentThread().interrupt();
}
return false;
}
- shutdown():停止接收新任務,原來的任務繼續執行
1、停止接收新的submit的任務;
2、已經提交的任務(包括正在跑的和佇列中等待的),會繼續執行完成;
3、等到第2步完成後,才真正停止;
- shutdownNow():停止接收新任務,原來的任務停止執行
1、跟 shutdown() 一樣,先停止接收新submit的任務;
2、忽略佇列裡等待的任務;
3、嘗試將正在執行的任務interrupt中斷;
4、返回未執行的任務列表;
說明:
它試圖終止執行緒的方法是透過呼叫 Thread.interrupt() 方法來實現的,這種方法的作用有限,如果執行緒中沒有sleep 、wait、Condition、定時鎖等應用, interrupt() 方法是無法中斷當前的執行緒的。
所以,shutdownNow() 並不代表執行緒池就一定立即就能退出,它也可能必須要等待所有正在執行的任務都執行完成了才能退出。但是大多數時候是能立即退出的。
- awaitTermination(long timeOut, TimeUnit unit):當前執行緒阻塞
當前執行緒阻塞,直到:
- 等所有已提交的任務(包括正在跑的和佇列中等待的)執行完;
- 或者 等超時時間到了(timeout 和 TimeUnit設定的時間);
- 或者 執行緒被中斷,丟擲InterruptedException
然後會監測 ExecutorService 是否已經關閉,返回true(shutdown請求後所有任務執行完畢)或false(已超時)
GrpcServerHelper#shutdownAndAwaitTermination
與ExecutorServiceHelper類中的shutdownAndAwaitTermination方法類似的,該方法將優雅的關閉grpcServer.
public static boolean shutdownAndAwaitTermination(final Server server, final long timeoutMillis) {
if (server == null) {
return true;
}
// disable new tasks from being submitted
server.shutdown();
final TimeUnit unit = TimeUnit.MILLISECONDS;
final long phaseOne = timeoutMillis / 5;
try {
// wait a while for existing tasks to terminate
if (server.awaitTermination(phaseOne, unit)) {
return true;
}
server.shutdownNow();
// wait a while for tasks to respond to being cancelled
if (server.awaitTermination(timeoutMillis - phaseOne, unit)) {
return true;
}
LOG.warn("Fail to shutdown grpc server: {}.", server);
} catch (final InterruptedException e) {
// (Re-)cancel if current thread also interrupted
server.shutdownNow();
// 保持中斷狀態
Thread.currentThread().interrupt();
}
return false;
}