併發任務啟動後,可能因超時、資源限制、用戶取消、異常等原因需要提前終止。Java **沒有安全的強制停止線程 API** (`Thread.stop()` 已棄用,釋放鎖導致數據不一致)。正確路徑:**協作式取消** — 任務定期檢查取消標誌,或響應**中斷信號**。本文通過遞歸掃描文件案例,層層遞進演示三種取消模式。
public class FileScanner implements Runnable {
private final File root;
private final List paths = new ArrayList<>();
public FileScanner(File root) {
if (root == null || !root.exists() || !root.isDirectory())
throw new IllegalArgumentException("root must be valid directory");
this.root = root;
}
@Override
public void run() {
traverse(root);
save(paths); // 簡化:同步保存
}
private void traverse(File dir) {
paths.add(dir.getAbsolutePath());
if (dir.isDirectory()) {
File[] children = dir.listFiles();
if (children != null) for (File child : children) traverse(child);
}
}
private void save(List paths) { /* 寫文件 */ }
}
// 危險示範
Thread t = new Thread(new FileScanner(new File("C:/")));
t.start();
Thread.sleep(1000);
t.stop(); // 強制釋放所有鎖,導致:
// 1. 寫入文件只寫一半 (數據損壞)
// 2. 集合內部狀態破壞 (ConcurrentModification 後遺症)
// 3. 鎖保護的不變量被破壞 (如資料庫連接池、緩存一致性)
後果不可控,生產環境零容忍。
public class CancellableFileScanner implements Runnable {
private final File root;
private final List paths = new ArrayList<>();
private volatile boolean cancelled = false; // 關鍵:volatile 保證可見性
public CancellableFileScanner(File root) { this.root = root; }
@Override
public void run() {
traverse(root);
if (!cancelled) save(paths);
}
private void traverse(File dir) {
if (cancelled) return; // 檢查點
paths.add(dir.getAbsolutePath());
if (dir.isDirectory()) {
File[] children = dir.listFiles();
if (children != null) for (File child : children) traverse(child);
}
}
public void cancel() { cancelled = true; }
private void save(List paths) { /* ... */ }
}
// 使用
CancellableFileScanner task = new CancellableFileScanner(new File("C:/"));
Thread t = new Thread(task);
t.start();
Thread.sleep(3000);
task.cancel(); // 請求取消,任務在下一個檢查點退出
// 修改 traverse:模擬耗時 IO
private void traverse(File dir) {
try { Thread.sleep(10_000); } // 模擬網絡/磁碟阻塞
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
if (cancelled) return; // 永遠執行不到這裡!
// ...
}
線程在 `sleep` 中阻塞,**無法檢查 `cancelled`**,取消請求發出後需等待阻塞結束 (此處 10 秒) 才能退出。
public class InterruptibleFileScanner implements Runnable {
private final File root;
private final List paths = new ArrayList<>();
public InterruptibleFileScanner(File root) { this.root = root; }
@Override
public void run() {
try {
traverse(root);
save(paths);
} catch (IOException e) { log.error(e); }
}
private void traverse(File dir) throws IOException {
// 1. 阻塞點:拋出 InterruptedException 直接返回
// 實際場景:Thread.sleep、queue.take、socket.read 等
// 此處演示:無阻塞,主動檢查
if (Thread.currentThread().isInterrupted()) return;
paths.add(dir.getAbsolutePath());
if (dir.isDirectory()) {
File[] children = dir.listFiles();
if (children != null) for (File child : children) traverse(child);
}
}
private void save(List paths) throws IOException { /* ... */ }
}
// 使用:調用 interrupt() 而非自定義 cancel()
Thread t = new Thread(new InterruptibleFileScanner(new File("C:/")));
t.start();
Thread.sleep(3000);
t.interrupt(); // 發送中斷信號
private void traverse(File dir) {
try {
// 假設這裡有阻塞操作:Thread.sleep(10000)
Thread.sleep(10_000);
} catch (InterruptedException e) {
// 關鍵:重新設置中斷狀位,讓外層檢查可見
Thread.currentThread().interrupt();
return; // 或拋出異常向上傳播
}
// 非阻塞路徑:主動檢查
if (Thread.currentThread().isInterrupted()) return;
paths.add(dir.getAbsolutePath());
// ...
}
為什麼要 `Thread.currentThread().interrupt()`? `sleep` 拋出 `InterruptedException` 時會**清除中斷狀位**。若不重新設置,後續 `isInterrupted()` 將返回 `false`,導致取消邏輯失效。
private void traverse(File dir) {
try {
Thread.sleep(10_000); // 阻塞點
} catch (InterruptedException e) {
return; // 直接退出,調用棧自然展開
}
if (Thread.currentThread().isInterrupted()) return;
// ...
}
本文通過文件掃描案例,演示了從「強制停止」→「布爾標誌」→「線程中斷」的演進。**中斷是 Java 取消任務的標準協議**,掌握「阻塞方法拋異常+清除狀位、非阻塞主動檢查、異常處理重新設置狀位」三部曲,是寫出可靠併發代碼的入門功力。下篇將實戰《Future/ExecutorService 取消框架、CompletableFuture 異步取消、線程池優雅關閉與中斷傳播》。