首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 程式設計入門

為 Java 應用程式新增關機鉤子:優雅關閉與資源釋放實戰

     編輯:Java 程式設計入門

應用場景

  • 關閉資料庫連線池、HTTP 客戶端連線池
  • 刷新快取到磁碟、持久化內存狀態
  • 註銷服務發現(Eureka、Nacos、Consul)
  • 優雅停止排程器、訊息隊列消費者
  • 釋放原生資源(檔案鎖、硬體介面、JNI 控制代碼)

核心 API


Runtime.getRuntime().addShutdownHook(Thread hook)
  • 向 JVM 註冊一個關機鉤子執行緒。
  • JVM 正常關閉時(System.exit()、使用者 Ctrl+C、SIGTERM、最後一個非守護執行緒結束),會並行啟動所有已註冊鉤子。
  • 不保證執行順序,鉤子間應無相依性。
  • 不會在強制終止(SIGKILL、Runtime.halt()、JVM 崩潰)時執行。
  • 鉤子執行期間再呼叫 System.exit() 會阻塞/死鎖;若需中止改用 Runtime.halt()。

完整範例


import java.io.*;
import java.util.Date;

public class GracefulShutdownDemo {
    public GracefulShutdownDemo() {
        registerShutdownHook();
    }

    private void registerShutdownHook() {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            try (FileWriter fw = new FileWriter("shutdown.log", true)) {
                String msg = "應用程式關閉時間: " + new Date() + System.lineSeparator();
                fw.write(msg);
                System.out.println("關機鉤子執行: " + msg.trim());
            } catch (IOException e) {
                System.err.println("關機日誌寫入失敗: " + e.getMessage());
            }
        }), "Shutdown-Logger");
    }

    public static void main(String[] args) throws InterruptedException {
        new GracefulShutdownDemo();
        System.out.println("應用程式啟動,按 Ctrl+C 或關閉終端機測試關機鉤子");
        // 模擬長時間運行
        Thread.sleep(Long.MAX_VALUE);
    }
}

Spring Boot 整合(現代標準做法)


@Component
public class GracefulShutdown implements DisposableBean {
    private final DataSource dataSource;
    private final RedisTemplate<String, Object> redisTemplate;
    private final Scheduler scheduler;

    public GracefulShutdown(DataSource dataSource,
                            RedisTemplate<String, Object> redisTemplate,
                            Scheduler scheduler) {
        this.dataSource = dataSource;
        this.redisTemplate = redisTemplate;
        this.scheduler = scheduler;
    }

    @Override
    public void destroy() {
        // 1. 停止接受新請求(若用 Tomcat 可配置 server.shutdown=graceful)
        // 2. 關閉排程器
        scheduler.shutdown();
        try { scheduler.awaitTermination(30, TimeUnit.SECONDS); }
        catch (InterruptedException ignored) {}

        // 3. 釋放連線池
        if (dataSource instanceof HikariDataSource hk) hk.close();

        // 4. 關閉 Redis 連線
        redisTemplate.getConnectionFactory().getConnection().close();
    }
}

或直接使用 @PreDestroy 標註方法,Spring 容器關閉時自動調用。

最佳實踐檢查清單

項目建議
冪等性鉤子邏輯必須冪等,重複執行不造成副作用
超時保護單一鉤子執行時間不宜過長,建議 < 10s;總關機逾時由 OS/容器控制
異常吞噬捕獲所有異常並記錄,避免一個鉤子崩潰影響其他鉤子
無相依鉤子間不應有順序相依;若有順序需求,合併為單一鉤子內部按序執行
守護執行緒鉤子執行緒本身不應設為 daemon,否則 JVM 可能在鉤子完成前退出
測試驗證單元測試模擬 System.exit() 驗證鉤子邏輯;整合測試用 Docker stop 驗證優雅關閉

常見誤區

  • 誤以為 addShutdownHook 能攔截 kill -9:SIGKILL 直接由 OS 終止進程,JVM 無機會執行任何代碼。
  • 在鉤子中啟動新非守護執行緒:會導致 JVM 無法退出(等待該執行緒結束)。
  • 重複註冊同一鉤子實例:同一 Thread 實例只能 start() 一次,重複註冊會拋 IllegalThreadStateException。
  • 靜態初始化塊中註冊:類載入過早可能導致資源尚未就緒;建議在 main() 或 Spring @PostConstruct 中註冊。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved