生產級 Java 應用必須妥善處理關閉時的資源釋放:關閉數據庫連接池、刷新緩存、持久化狀態、註銷服務發現等。Runtime.addShutdownHook() 是 JVM 提供的標準機制,本文深度解析其原理、陷阱與工程化用法。
public class Runtime {
// 註冊關閉鉤子,JVM 關閉時並發啟動所有掛鉤線程
public void addShutdownHook(Thread hook)
// 移除已註冊鉤子,返回是否成功
public boolean removeShutdownHook(Thread hook)
// 觸發關閉序列 (正常關閉)
public void exit(int status)
// 強制終止 (不運行鉤子)
public void halt(int status)
}
System.exit()、用戶 Ctrl+C (SIGINT)Runtime.halt()、kill -9 (SIGKILL)、JVM 崩潰、OS 強制終止System.exit() (死鎖)public class GracefulShutdownDemo {
public static void main(String[] args) {
// 1. 註冊鉤子 (推薦靜態代碼塊/構造器中單次註冊)
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("[ShutdownHook] 開始清理資源...");
try {
// 模擬資源釋放
Thread.sleep(1000);
Files.writeString(Paths.get("shutdown.log"),
"應用關閉時間: " + Instant.now() + "\n",
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
System.out.println("[ShutdownHook] 資源清理完成");
} catch (Exception e) {
System.err.println("[ShutdownHook] 清理失敗: " + e.getMessage());
}
}, "MyApp-ShutdownHook"));
// 2. 主業務邏輯
System.out.println("應用運行中... 按 Ctrl+C 測試優雅關閉");
try { Thread.currentThread().join(); } catch (InterruptedException ignored) {}
}
}
避免重複註冊、支持優先級、超時控制、依賴順序:
public final class ShutdownHookManager {
private static final ShutdownHookManager INSTANCE = new ShutdownHookManager();
private final List hooks = new CopyOnWriteArrayList<>();
private final Thread shutdownThread;
private volatile boolean shuttingDown = false;
private ShutdownHookManager() {
shutdownThread = new Thread(this::executeHooks, "ShutdownHookManager");
Runtime.getRuntime().addShutdownHook(shutdownThread);
}
public static ShutdownHookManager getInstance() { return INSTANCE; }
/** 註冊關閉任務
* @param name 任務名稱
* @param priority 優先級 (數值越小越先執行)
* @param timeoutMs 最大等待時間
* @param task 實際清理邏輯
*/
public void registerHook(String name, int priority, long timeoutMs, Runnable task) {
if (shuttingDown) throw new IllegalStateException("已進入關閉流程,禁止註冊");
hooks.add(new HookTask(name, priority, timeoutMs, task));
hooks.sort(Comparator.comparingInt(HookTask::getPriority));
}
public boolean unregisterHook(Runnable task) {
return hooks.removeIf(h -> h.task == task);
}
private void executeHooks() {
shuttingDown = true;
System.out.println("[ShutdownManager] 開始執行 " + hooks.size() + " 個關閉任務");
for (HookTask hook : hooks) {
Thread taskThread = new Thread(() -> {
try {
hook.task.run();
System.out.println("[ShutdownManager] 任務 [" + hook.name + "] 完成");
} catch (Exception e) {
System.err.println("[ShutdownManager] 任務 [" + hook.name + "] 異常: " + e);
}
}, hook.name);
taskThread.start();
try {
taskThread.join(hook.timeoutMs);
if (taskThread.isAlive()) {
System.err.println("[ShutdownManager] 任務 [" + hook.name + "] 超時 " + hook.timeoutMs + "ms,強制繼續");
// 注意:無法安全中斷線程,僅記錄繼續
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println("[ShutdownManager] 所有關閉任務處理完畢");
}
private static class HookTask {
final String name; final int priority; final long timeoutMs; final Runnable task;
HookTask(String n, int p, long t, Runnable tk) { name=n; priority=p; timeoutMs=t; task=tk; }
int getPriority() { return priority; }
}
}
@Component
public class ResourceCleanupConfig {
@Autowired DataSource dataSource;
@Autowired RedisTemplate redisTemplate;
@Autowired ServiceRegistry registry;
@PostConstruct
public void registerShutdownHooks() {
ShutdownHookManager mgr = ShutdownHookManager.getInstance();
// 優先級 10: 註銷服務 (最快完成)
mgr.registerHook("ServiceDeregister", 10, 5000, () -> {
registry.deregister(localServiceInstance);
});
// 優先級 20: 刷新緩存
mgr.registerHook("CacheFlush", 20, 10000, () -> {
redisTemplate.getConnectionFactory().getConnection().flushDb();
});
// 優先級 30: 關閉連接池 (耗時最長)
mgr.registerHook("DataSourceClose", 30, 30000, () -> {
if (dataSource instanceof HikariDataSource) {
((HikariDataSource) dataSource).close();
}
});
}
}
| 框架 | 機制 | 對應鉤子 |
|---|---|---|
| Spring | ConfigurableApplicationContext.close() | 註冊 DisposableBean/@PreDestroy Bean |
| Spring Boot | 監聽 ContextClosedEvent | 自動註冊 JVM Shutdown Hook 調用 close() |
| Netty | EventLoopGroup.shutdownGracefully() | 在鉤子中調用 |
| Dubbo | Protocol.destroy() | 註銷服務、關閉連接 |
原則:優先使用框架提供的生命周期回調,僅在框架未覆蓋場景 (獨立線程池、原生 Socket、JNI 資源) 直接操作 Shutdown Hook。
close() 多次調用安全)System.exit(0) 驗證鉤子執行 (需 SecurityManager 允許)stop 發送 SIGTERM (10s 默認超時) → SIGKILL,需確保清理在 10s 內完成preStop 鉤子 + terminationGracePeriodSeconds 提供更精細控制-XX:+UseContainerSupport 確保容器內正確識別 CPU/內存限制建立統一的 ShutdownHookManager,將資源清理邏輯標準化、可視化、可測試,是 Java 應用工程化成熟度的重要標誌。