優雅地停止執行中的線程是並發編程核心難題。Java **不提供**安全的 `stop()`/`suspend()` (已棄用),標準機制是 **協作式中斷**:線程定期檢查中斷狀態,自行決定何時退出。本文對比 `volatile boolean` 標誌與 `Thread.interrupt()` 兩種模式,解析阻塞場景下的處理藝術。
| 維度 | volatile boolean 旗標 | Thread.interrupt() 機制 |
|---|---|---|
| 本質 | 用戶級協議,需手動檢查 | JVM 內建、語言級支援 |
| 阻塞方法響應 | **無法**中斷 sleep/wait/join/IO | **自動**喚醒並拋 InterruptedException |
| 狀態查詢 | 讀取變數 | `Thread.currentThread().isInterrupted()` / 靜態 `interrupted()` |
| 適用場景 | 純計算循環、非阻塞邏輯 | **所有場景**,含阻塞 IO、鎖、Channel |
| 代碼侵入性 | 需在循環處插入檢查 | 僅需捕獲 InterruptedException |
class CalculationWorker extends Thread {
private volatile boolean running = true; // 關鍵:volatile 保證可見性
@Override public void run() {
while (running) { // 每次迴圈檢查
// 耗時計算...
processData();
}
System.out.println("計算線程正常退出");
}
public void shutdown() { running = false; }
}
// 使用
CalculationWorker w = new CalculationWorker();
w.start();
// ...
w.shutdown(); // 設置旗標,下一次循環檢查到退出
關鍵點: `volatile` 確保主線程寫入 `false` 能被工作線程**立即看到** (不會被 JIT 優化緩存到寄存器)。
class BlockingWorker extends Thread {
private volatile boolean running = true;
@Override public void run() {
while (running) {
try {
// 模擬長時間阻塞:睡眠、等待鎖、網路 IO、Queue.take()
Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {
// **關鍵:中斷異常會清除中斷狀態!**
// 必須重新設置旗標或重新中斷當前線程
running = false; // 方案 A:配合旗標退出循環
// Thread.currentThread().interrupt(); // 方案 B:保留中斷狀態向上傳遞
System.out.println("阻塞被中斷喚醒,準備退出");
}
}
}
public void shutdown() {
this.interrupt(); // 唯一能打破阻塞的方式
}
}
核心規則:
public class InterruptibleTask implements Runnable {
@Override
public void run() {
try {
while (!Thread.currentThread().isInterrupted()) { // 條件 1:檢查中斷狀態
// 1. 非阻塞邏輯:定期檢查
doWork();
// 2. 可中斷阻塞調用:拋出 InterruptedException 自動跳出
Thread.sleep(1000); // 或 queue.take(), lock.lockInterruptibly()
}
} catch (InterruptedException e) {
// 3. 捕獲中斷異常:重新設置中斷狀態 (供上層調用者感知)
Thread.currentThread().interrupt();
} finally {
// 4. 清理資源:關閉連接、釋放鎖、寫入檢查點
cleanup();
}
}
private void doWork() { /* ... */ }
private void cleanup() { /* ... */ }
}
// 使用:ExecutorService 管理
ExecutorService executor = Executors.newSingleThreadExecutor();
Future> future = executor.submit(new InterruptibleTask());
// ... 需要停止時
future.cancel(true); // 參數 true = mayInterruptIfRunning → 呼叫 thread.interrupt()
executor.shutdown();
| 方法 | 簽名 | 行為 | 典型用法 |
|---|---|---|---|
| 實例方法 | `thread.interrupt()` | 設置目標線程中斷狀態為 true;若線程阻塞於可中斷方法,喚醒並拋 InterruptedException | 外部請求停止線程 |
| 實例方法 | `thread.isInterrupted()` | **不清除**中斷狀態,返回當前狀態 | 線程內部輪詢檢查 / 外部查詢狀態 |
| 靜態方法 | `Thread.interrupted()` | **清除**當前線程中斷狀態,返回舊值 | 線程內部消費中斷信號 (如 catch 區塊重新標記前) |
| 錯誤代碼 | 後果 | 修正 |
|---|---|---|
| `while(true) { if(flag) break; }` 無 volatile | JIT 可能優化讀取旗標到寄存器,主線程修改不可見 → 無限循環 | 加 `volatile` 或用 `interrupt()` |
| `catch (InterruptedException e) {}` 空 catch | 中斷信號丟失,線程繼續運行,`future.cancel(true)` 無效 | 重新 `interrupt()` 或設置退出旗標 |
| 用 `Thread.stop()` 強制終止 | 釋放鎖不一致、對象損壞、數據竄改 (已棄用、不安全) | 協作式中斷 + 可中斷阻塞調用 |
| 在 `synchronized` 區塊中長時間計算 | `interrupt()` 無法打破 `synchronized` 獲取鎖阻塞 (Java 鎖不可中斷) | 用 `Lock.lockInterruptibly()` 替代 `synchronized` |
| IO 阻塞 (傳統 BIO `InputStream.read()`) | `interrupt()` 無法喚醒 (Java 1.4 前) | 用 NIO `Channel` + `Selector` / `AsynchronousFileChannel` / 關閉底層 Socket |
掌握線程中斷機制,是從「寫出能跑的多線程代碼」進階到「寫出生產級、可優雅關閉、可觀測的並發系統」的分水嶺。