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

Java 併發基礎實踐:任務取消與線程中斷機制深度解析 (上)

     編輯:Java 相關知識

前言:啟動容易,優雅退出難

併發任務啟動後,可能因超時、資源限制、用戶取消、異常等原因需要提前終止。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.stop() —— 絕對禁止


// 危險示範
Thread t = new Thread(new FileScanner(new File("C:/")));
t.start();
Thread.sleep(1000);
t.stop(); // 強制釋放所有鎖,導致:
// 1. 寫入文件只寫一半 (數據損壞)
// 2. 集合內部狀態破壞 (ConcurrentModification 後遺症)
// 3. 鎖保護的不變量被破壞 (如資料庫連接池、緩存一致性)

後果不可控,生產環境零容忍。

模式二:volatile boolean 取消標誌 —— 基礎協作


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 秒) 才能退出。

模式三:Thread.interrupt() 中斷機制 —— 標準協作

核心原理

  • `thread.interrupt()`:設置線程**中斷狀位** = true,不會立即停止線程。
  • 阻塞方法 (`Thread.sleep/wait/join`、`Selector.select`、`BlockingQueue.take/put`、`ServerSocket.accept`、IO 流讀寫) 檢測到中斷狀位 → **拋出 `InterruptedException`** 並**清除中斷狀位 (重置為 false)**。
  • 非阻塞代碼需**主動檢查** `Thread.currentThread().isInterrupted()` 或 `Thread.interrupted()` (檢查並清除)。

改造:響應中斷的遍歷


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(); // 發送中斷信號

進階:統一用中斷狀位控制 (無需自定義 cancelled)


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;
    // ...
}

中斷最佳實踐清單

  1. 任務代碼必須協作: 定期檢查 `isInterrupted()`,或正確處理 `InterruptedException` (重新設置狀位/向上傳播/退出)。
  2. 不要吞沒中斷: `catch (InterruptedException e) {}` 是大忌,至少 `Thread.currentThread().interrupt()`。
  3. 庫代碼規範: 實現 `BlockingQueue`/`Lock`/`Channel` 等阻塞 API 時,必須響應中斷並拋出 `InterruptedException`。
  4. 取消傳播: 父任務取消 → 級聯 `interrupt()` 子線程/線程池 (`executor.shutdownNow()`)。
  5. 不可中斷區域: 持有鎖更新共享狀態的關鍵區,用 `Lock.lockInterruptibly()` 而非 `synchronized`,或短暫屏蔽中斷 (極少見)。

結語

本文通過文件掃描案例,演示了從「強制停止」→「布爾標誌」→「線程中斷」的演進。**中斷是 Java 取消任務的標準協議**,掌握「阻塞方法拋異常+清除狀位、非阻塞主動檢查、異常處理重新設置狀位」三部曲,是寫出可靠併發代碼的入門功力。下篇將實戰《Future/ExecutorService 取消框架、CompletableFuture 異步取消、線程池優雅關閉與中斷傳播》。

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