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

Java 線程優雅關閉的三種核心模式:狀態位、中斷機制、資源釋放

     編輯:Java 程式設計入門

前言:為什麼不能用 Thread.stop()?

Thread.stop()、suspend()、resume() 自 JDK 1.2 起被標記為 @Deprecated,原因是它們強制釋放鎖、破壞對象不變量、導致數據競態與死鎖。正確的線程關閉必須遵循協作式原則:線程主動檢查關閉信號、清理資源、有序退出。

本文系統梳理三大核心模式,並給出生產級代碼模板。

一、模式一:volatile 狀態位(適用於非阻塞任務)

核心代碼

public class FlagTask extends Thread {
    // volatile 保證跨線程可見性,防止 JIT 將標誌快取寄存器導致不可見
    private volatile boolean running = true;

    public void shutdown() {
        running = false;
    }

    @Override
    public void run() {
        while (running) {
            // 執行非阻塞業務邏輯
            doWork();
        }
        // 清理資源、記錄日誌
        cleanup();
    }

    private void doWork() { /* 計算、輪詢、發送心跳等 */ }
    private void cleanup() { /* 關閉連接、刷新緩衝區 */ }
}

適用場景與限制

優點缺點適用
簡單、無異常、響應即時無法中斷阻塞調用(sleep, wait, I/O, Lock)純計算循環、輪詢任務、非阻塞隊列消費

進階:使用 AtomicBoolean 替代 volatile boolean

private final AtomicBoolean running = new AtomicBoolean(true);
public void shutdown() { running.set(false); }
while (running.get()) { ... }

語義更清晰,支援 CAS 操作(如 compareAndSet(true, false) 實現「只關閉一次」)。

二、模式二:Thread.interrupt() 中斷機制(適用於可中斷阻塞)

核心原理

  • 每個線程擁有 中斷狀態標誌(boolean interrupted)。
  • Thread.interrupt() 設置該標誌為 true。
  • 若線程正在調用 Object.wait()、Thread.sleep()、Thread.join()、Selector.select()、InterruptibleChannel 操作,這些方法會拋出 InterruptedException 並清除中斷標誌。
  • 若線程在運行中(非阻塞),標誌保持 true,需自行檢查 Thread.interrupted()(檢查並清除)或 isInterrupted()(僅檢查)。

標準模板:可中斷阻塞任務

public class InterruptibleTask implements Runnable {
    @Override
    public void run() {
        try {
            while (!Thread.currentThread().isInterrupted()) {
                // 1. 非阻塞工作
                doWork();
                
                // 2. 可中斷阻塞點(sleep/wait/阻塞隊列 take)
                Thread.sleep(1000); // 或 blockingQueue.take()
            }
        } catch (InterruptedException e) {
            // 3. 捕獲中斷異常 → 確認關閉意圖 → 清理 → 可選:重新設置中斷標誌傳播
            Thread.currentThread().interrupt(); // 重要:恢復中斷狀態給上層調用者
        } finally {
            cleanup(); // 釋放鎖、關閉連接、刷新數據
        }
    }
    // ...
}

關鍵最佳實踐

  1. 不要吞噬 InterruptedException: 必須在 catch 塊中 Thread.currentThread().interrupt() 重新設置標誌,或拋出異常向上傳播。否則中斷信號丟失,上層無法感知。
  2. 檢查點策略: 長循環中每次迭代檢查 isInterrupted();阻塞調用依賴異常拋出。
  3. LockSupport.park(): 現代並發包(Lock、Condition、CompletableFuture)底層用 LockSupport.park(),同樣響應中斷(返回而不拋異常,需自檢 Thread.interrupted())。

不可中斷阻塞的處理(I/O、鎖)

傳統 BIO (InputStream.read, ServerSocket.accept) 不響應中斷。解決方案見模式三。

三、模式三:資源關閉觸發異常(適用於不可中斷阻塞 I/O)

核心思想

關閉線程持有的底層資源(Socket、Channel、鎖),導致阻塞調用拋出異常(SocketException: Socket closed、AsynchronousCloseException、ClosedByInterruptException),從而強制線程從阻塞中返回。

示例:關閉 ServerSocket 釋放 accept() 阻塞

public class SocketServerTask implements Runnable {
    private volatile ServerSocket serverSocket;

    @Override
    public void run() {
        try {
            serverSocket = new ServerSocket(3333);
            while (!Thread.currentThread().isInterrupted()) {
                Socket client = serverSocket.accept(); // 阻塞點
                handleClient(client);
            }
        } catch (IOException e) {
            // 正常關閉路徑:Socket closed / AsynchronousCloseException
            if (!(e instanceof SocketException && e.getMessage().contains("Socket closed"))) {
                e.printStackTrace(); // 真正錯誤
            }
        } finally {
            closeQuietly(serverSocket);
        }
    }

    public void shutdown() {
        closeQuietly(serverSocket); // 觸發 accept() 拋異常
    }

    private void closeQuietly(Closeable c) {
        if (c != null) try { c.close(); } catch (IOException ignored) {}
    }
}

NIO 非阻塞模式(推薦)

// Selector 可被 interrupt() 喚醒,或 close() 關閉
Selector selector = Selector.open();
selector.wakeup(); // 或 selector.close()

JDBC 數據庫連接

// Statement.cancel() / Connection.close() 可中斷正在執行的查詢
statement.cancel(); // 標準 JDBC 4.0+
connection.close();

四、三種模式對比與選型決策樹

維度volatile 旗標interrupt()資源關閉
阻塞類型無阻塞 / 非阻塞隊列可中斷阻塞 (sleep, wait, park, NIO, Lock)不可中斷阻塞 (BIO I/O, synchronized, 外部庫)
響應延遲下次循環檢查立即 (拋異常) 或下次檢查點立即 (底層資源關閉觸發異常)
代碼侵入性低 (while 判斷)中 (try-catch, 恢復標誌)高 (需持有資源引用, 區分正常/異常關閉)
線程安全volatile 保證可見性JVM 內建原子操作需同步資源訪問
組合使用✅ 基礎✅ 核心✅ 最後手段

決策流程:

  1. 任務無阻塞調用 → volatile 旗標。
  2. 任務有 sleep/wait/join/park/NIO/Lock → interrupt() + 旗標雙重保險。
  3. 任務有 BIO I/O / synchronized / 第三方同步庫 → 資源關閉 + interrupt()。
  4. 現代代碼優先使用 NIO / 並發包 / 結構化並發 (JEP 453),天然支援中斷。

五、生產級通用關閉框架

public abstract class GracefulTask implements Runnable {
    private final AtomicBoolean shutdownRequested = new AtomicBoolean(false);
    private volatile Thread runner;

    public final void start() {
        runner = new Thread(this, getName());
        runner.start();
    }

    public final void shutdown() {
        if (shutdownRequested.compareAndSet(false, true)) {
            Thread t = runner;
            if (t != null) {
                t.interrupt(); // 嘗試中斷
                closeResources(); // 關閉底層資源 (Socket, Channel, Selector, Connection)
            }
        }
    }

    protected abstract void closeResources(); // 子類實現具體資源關閉

    @Override
    public final void run() {
        try {
            execute(); // 子類實現業務邏輯
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt(); // 恢復標誌
        } catch (Exception e) {
            handleError(e);
        } finally {
            cleanup();
        }
    }

    protected abstract void execute() throws InterruptedException;
    protected void handleError(Exception e) { log.error("Task error", e); }
    protected void cleanup() { /* 統一清理 */ }
    
    // 供 execute() 中長循環調用
    protected final boolean shouldRun() {
        return !shutdownRequested.get() && !Thread.currentThread().isInterrupted();
    }
}

六、結構化並發(Java 21+ JEP 453 / 480)

// StructuredTaskScope (Java 21 Preview)
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<String> f1 = scope.fork(() -> fetchUrl(url1));
    Future<Integer> f2 = scope.fork(() -> compute());
    scope.join();           // 等待所有完成
    scope.throwIfFailed();  // 任一失敗拋異常
    
    String result1 = f1.resultNow();
    int result2 = f2.resultNow();
} // 作用域結束自動關閉所有子任務 (interrupt + 關閉資源)

結構化並發將「生命週期綁定到代碼塊」作為語言級特性,徹底解決線程洩漏、關閉傳播難題,是未來標準。

結語

Java 線程關閉的本質是協作式取消。沒有銀彈,只有根據阻塞類型選擇volatile 旗標、interrupt()、資源關閉的組合拳。現代開發應:

  1. 優先使用支援中斷的 API(NIO、Lock、CompletableFuture、StructuredTaskScope)。
  2. 封裝通用關閉框架,統一異常處理、日誌、指標上報。
  3. 嚴禁 Thread.stop()、suspend()、吞噬 InterruptedException。
  4. 單元測試覆蓋:正常完成、中斷關閉、超時關閉、異常關閉四條路徑。

掌握這三種模式,讓你的多線程系統在部署、升級、故障恢復時都能體面地「說再見」。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved