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

Java 線程正確終止與恢復(續):深度掌握中斷機制與協作式取消模式

     編輯:Java 綜合教學

Java 線程正確終止與恢復(續):中斷機制本質與最佳實踐

上一篇介紹了 volatile 標志位與 interrupt() 基礎用法。本文深入剖析 Java 中斷機制的底層語義、阻塞方法的響應行為、以及構建健壯取消邏輯的完整模式。

一、重新認識中斷:協作而非搶佔

Java 不提供搶佔式安全終止線程的 API(如已棄用的 stop())。取而代之的是協作式中斷機制:

  • 每個線程擁有一個 boolean 中斷狀態(初始 false)。
  • interrupt() 將狀態置 true,不會立即停止線程,只是發出「請求中斷」信號。
  • 線程在下一個可中斷點(如調用 sleep()、wait()、BlockingQueue.take())檢測到狀態,拋出 InterruptedException 並清除中斷狀態。
  • 線程自行決定如何處理:退出、重試、記錄日誌等。

二、標志位失效的經典場景:阻塞方法不檢查標志


// 生產者速度遠快於消費者,隊列滿時 put() 阻塞
while (on) { // volatile boolean on = true;
    queue.put(item); // 阻塞在此,無法檢查 on == false
}
cancel() { on = false; } // 設置後,生產者仍卡在 put(),線程永不結束

結論:單純 volatile 標志位無法中斷阻塞在 I/O、鎖、隊列操作中的線程。

三、正確模式:中斷機制 + 標志位雙重保障


public void run() {
    try {
        while (on && !Thread.currentThread().isInterrupted()) {
            queue.put(item); // 可中斷阻塞方法
        }
    } catch (InterruptedException e) {
        // 關鍵:恢復中斷狀態,讓上層調用者可感知
        Thread.currentThread().interrupt();
        return; // 或拋出異常
    }
}
public void cancel() {
    on = false;
    interrupt(); // 喚醒阻塞線程
}

關鍵點:

  1. 循環條件雙重判斷 on && !isInterrupted()。
  2. 捕獲 InterruptedException 後 必須恢復中斷狀態(Thread.currentThread().interrupt()),否則上層調用者無法感知中斷請求。
  3. 調用 interrupt() 打破阻塞。

四、FutureTask 與 Executor 框架的取消支持


Future<?> task = executor.submit(runnable);
try {
    task.get(5, TimeUnit.SECONDS); // 超時取消
} catch (TimeoutException e) {
    task.cancel(true); // true = mayInterruptIfRunning
} finally {
    // 清理資源
}
  • cancel(true):若任務正在執行,發送中斷;若未啟動,不再啟動。
  • cancel(false):僅不啟動未執行任務,不中斷正在運行的。

五、完整實戰:可靠關閉的日誌服務

場景:多生產者寫入隊列,單消費者異步落盤。需保證 關閉時已提交日志全部寫入,且無新日志接入。


public class LoggerService {
    private final BlockingQueue<String> logQueue = new LinkedBlockingQueue<>(5);
    private final LoggerThread loggerThread;
    private volatile boolean isShutdown = false;
    private int reservations = 0; // 正在處理的生產者計數

    public void recordLog(String msg) throws InterruptedException {
        synchronized (this) {
            if (isShutdown) throw new IllegalStateException("已關閉");
            ++reservations;
        }
        logQueue.put(msg); // 阻塞直到有空位
    }

    public void stop() {
        synchronized (this) { isShutdown = true; }
        loggerThread.interrupt(); // 打破 take() 阻塞
    }

    private class LoggerThread extends Thread {
        public void run() {
            try {
                while (true) {
                    String msg;
                    try {
                        synchronized (LoggerService.this) {
                            if (isShutdown && reservations == 0) break; // 雙重檢查
                        }
                        msg = logQueue.take(); // 可中斷阻塞
                        synchronized (LoggerService.this) { --reservations; }
                        writer.println("Read: " + msg);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt(); // 恢復中斷狀態
                    }
                }
            } finally { writer.close(); }
        }
    }
}

設計亮點:

  • reservations 計數器確保「關閉請求發出後,不再接受新日志,且等待在途日志處理完畢」。
  • 消費者捕獲中斷異常後恢復中斷狀態,保持線程中斷語義傳遞。
  • 生產者 recordLog 先原子檢查關閉標志再增加計數,避免競態。

六、總結:線程終止最佳實踐清單

  1. 優先使用 中斷機制(interrupt() + InterruptedException)而非單純標志位。
  2. 阻塞方法必須是可中斷的(BlockingQueue、Thread.sleep、Lock.lockInterruptibly 等)。
  3. 捕獲 InterruptedException 後 恢復中斷狀態 或 拋出異常,切勿吞噬(printStackTrace() 即吞噬)。
  4. 使用 Future.cancel(true) / 線程池 shutdownNow() 統一管理任務生命週期。
  5. 關閉邏輯採用 「拒絕新任務 + 等待在途任務 + 中斷阻塞」 三步走。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved