Java 中斷異常的正確處理方式

處理InterruptedException

這個故事可能很熟悉:你正在寫一個測試程序,你需要暫停某個線程一段時間,所以你調用 Thread.sleep()。然後編譯器或 IDE 就會抱怨說 InterruptedException 沒有拋出聲明或捕獲。什麼是 InterruptedException,你爲什麼要處理它?

最常見的響應 InterruptedException 做法是吞下它 - 捕獲它並且什麼也不做(或者記錄它,也沒好多少) - 正如我們將在清單4中看到的那樣。不幸的是,這種方法拋棄了關於中斷髮生的重要信息,這可能會損害應用程序取消活動或響應及時關閉的能力。

阻塞方法

當一個方法拋出 InterruptedException 時,意味着幾件事情: 除了它可以拋出一個特定的檢查異常, 它還告訴你它是一種阻塞方法,它會嘗試解除阻塞並提前返回。

阻塞方法不同於僅需要很長時間才能運行完成的普通方法。普通方法的完成僅取決於你要求它做多少事以及是否有足夠的計算資源(CPU週期和內存)。另一方面,阻塞方法的完成還取決於某些外部事件,例如計時器到期,I/O 完成或另一個線程的操作(釋放鎖,設置標誌或放置任務到工作隊列)。普通方法可以在完成工作後立即結束,但阻塞方法不太好預測,因爲它們依賴於外部事件。

因爲如果他們正在等待永遠不會在事件,發生堵塞的方法有可能永遠不結束,常用在阻塞可取消的操作。對於長時間運行的非阻塞方法,通常也是可以取消的。可取消操作是可以在通常自行完成之前從外部強制移動到完成狀態的操作。 Thread提供的Thread.sleep() 和 Object.wait() 方法中斷機制是一種取消線程繼續阻塞的機制; 它允許一個線程請求另一個線程提前停止它正在做的事情。當一個方法拋出時 InterruptedException,它告訴你如果執行方法的線程被中斷,它將嘗試停止它正在做的事情提前返回, 並通過拋出 InterruptedException 表明它的提早返回。表現良好的阻塞庫方法應該響應中斷並拋出 InterruptedException 異常, 以便它們可以應用在可取消的活動中而不會妨礙程序的響應性。

線程中斷

每個線程都有一個與之關聯的布爾屬性,表示其中斷狀態。中斷狀態最初爲假; 當某個線程被其他線程通過調用中斷 Thread.interrupt() 時, 會發生以下兩種情況之一: 如果該線程正在執行低級別的中斷阻塞方法 Thread.sleep(),Thread.join()或 Object.wait()等,它取消阻塞並拋出 InterruptedException。除此以外,interrupt() 僅設置線程的中斷狀態。在中斷的線程中運行的代碼可以稍後輪詢中斷的狀態以查看是否已經請求停止它正在做的事情; 中斷狀態可以通過 Thread.isInterrupted() 讀取,並且可以在命名不佳的單個操作Thread.interrupted()中讀取和清除 。

中斷是一種合作機制。當一個線程中斷另一個線程時,被中斷的線程不一定會立即停止它正在做的事情。相反,中斷是一種禮貌地要求另一個線程在方便的時候停止它正在做什麼的方式。有些方法,比如Thread.sleep()認真對待這個請求,但方法不一定要注意中斷請求。不阻塞但仍可能需要很長時間才能執行完成的方法可以通過輪詢中斷狀態來尊重中斷請求,並在中斷時提前返回。你可以自由地忽略中斷請求,但這樣做可能會影響響應速度。

中斷的合作性質的一個好處是它爲安全地構建可取消的活動提供了更大的靈活性。我們很少想立即停止活動; 如果活動在更新期間被取消,程序數據結構可能會處於不一致狀態。中斷允許可取消活動清理正在進行的任何工作,恢復不變量,通知其他活動取消事件,然後終止。

處理InterruptedException

如果 throw InterruptedException 意味着這個方法是一個阻塞方法,那麼調用一個阻塞方法意味着你的方法也是一個阻塞方法,你應該有一個處理策略 InterruptedException。通常最簡單的策略是你自己也拋出 InterruptedException 異常,如清單1 中的 putTask() 和 getTask() 方法所示。這樣做會使你的方法響應中斷,並且通常只需要添加 InterruptedException 到 throws 子句。

清單1.通過不捕獲它來向調用者傳播InterruptedException

public class TaskQueue {    private static final int MAX_TASKS = 1000; 
    private BlockingQueue<Task> queue 
        = new LinkedBlockingQueue<Task>(MAX_TASKS); 
    public void putTask(Task r) throws InterruptedException { 
        queue.put(r);
    } 
    public Task getTask() throws InterruptedException { 
        return queue.take();
    }
}

有時在傳播異常之前需要進行一些清理。在這種情況下,你可以捕獲 InterruptedException,執行清理,然後重新拋出異常。清單2是一種用於匹配在線遊戲服務中的玩家的機制,說明了這種技術。該 matchPlayers() 方法等待兩個玩家到達然後開始新遊戲。如果在一個玩家到達之後但在第二個玩家到達之前它被中斷,則在重新投擲之前將該玩家放回隊列 InterruptedException,以便玩家的遊戲請求不會丟失。

清單2.在重新拋出 InterruptedException 之前執行特定於任務的清理

public class PlayerMatcher {    private PlayerSource players; 
    public PlayerMatcher(PlayerSource players) { 
        this.players = players; 
    } 
    public void matchPlayers() <strong>throws InterruptedException</strong> { 
        Player playerOne, playerTwo;         try {             while (true) {
                 playerOne = playerTwo = null;                 // 等待兩個玩家到來以便開始遊戲
                 playerOne = players.waitForPlayer(); // 會拋出中斷異常
                 playerTwo = players.waitForPlayer(); // 會拋出中斷異常
                 startNewGame(playerOne, playerTwo);
             }
         }         catch (InterruptedException e) {  
             // 如一個玩家中斷了, 將這個玩家放回隊列
             if (playerOne != null)
                 players.addFirst(playerOne);             // 然後傳播異常
             throw e;
         }
    }
}

不要吞下中斷

有時拋出 InterruptedException 不是一種選擇,例如當通過 Runnable 調用可中斷方法定義的任務時。在這種情況下,你不能重新拋出 InterruptedException,但你也不想做任何事情。當阻塞方法檢測到中斷和拋出時 InterruptedException,它會清除中斷狀態。如果你抓住 InterruptedException 但不能重新拋出它,你應該保留中斷髮生的證據,以便調用堆棧上的代碼可以瞭解中斷並在需要時響應它。此任務通過調用 interrupt()實現“重新中斷”當前線程,如清單3所示。至少,無論何時捕獲 InterruptedException 並且不重新拋出它,都要在返回之前重新中斷當前線程。

清單3.捕獲InterruptedException後恢復中斷狀態

public class TaskRunner implements Runnable {
    private BlockingQueue<Task> queue; 
    public TaskRunner(BlockingQueue<Task> queue) { 
        this.queue = queue; 
    } 
    public void run() { 
        try {             while (true) {
                 Task task = queue.take(10, TimeUnit.SECONDS);
                 task.execute();
             }
         }         catch (InterruptedException e) { 
             //重要: 恢復中斷狀態
             Thread.currentThread().interrupt();
         }
    }
}

你可以做的最糟糕的事情 InterruptedException 就是吞下它 - 抓住它,既不重新拋出它也不重新確定線程的中斷狀態。處理你沒有規劃的異常的標準方法 - 捕獲它並記錄它 - 也算作吞噬中斷,因爲調用堆棧上的代碼將無法找到它。(記錄 InterruptedException 也很愚蠢,因爲當人類讀取日誌時,對它做任何事都爲時已晚。)清單4顯示了吞下中斷的常見模式:

清單4.吞下中斷 - 不要這樣做

// 不要這麼做!public class TaskRunner implements Runnable {
    private BlockingQueue<Task> queue; 
    public TaskRunner(BlockingQueue<Task> queue) { 
        this.queue = queue; 
    } 
    public void run() { 
        try {             while (true) {
                 Task task = queue.take(10, TimeUnit.SECONDS);
                 task.execute();
             }
         }         catch (InterruptedException swallowed) { 
             /* DON'T DO THIS - RESTORE THE INTERRUPTED STATUS INSTEAD */
             /* 不要這麼做 - 要讓線程中斷 */

         }
    }
}

如果你不能重新拋出 InterruptedException,無論你是否計劃對中斷請求執行操作,你仍然希望重新中斷當前線程,因爲單箇中斷請求可能有多個“收件人”。標準線程池(ThreadPoolExecutor)工作線程實現響應中斷,因此中斷線程池中運行的任務可能具有取消任務和通知執行線程線程池正在關閉的效果。如果作業吞下中斷請求,則工作線程可能不會知道請求了中斷,這可能會延遲應用程序或服務關閉。

實施可取消的任務

語言規範中沒有任何內容給出任何特定語義的中斷,但在較大的程序中,除了取消之外,很難保持中斷的任何語義。根據活動,用戶可以通過 GUI 或通過 JMX 或 Web 服務等網絡機制請求取消。它也可以由程序邏輯請求。例如,如果 Web 爬蟲檢測到磁盤已滿,則可能會自動關閉自身,或者並行算法可能會啓動多個線程來搜索解決方案空間的不同區域,並在其中一個找到解決方案後取消它們。

僅僅因爲一個任務是取消並不意味着它需要一箇中斷請求響應立即。對於在循環中執行代碼的任務,通常每次循環迭代僅檢查一次中斷。根據循環執行的時間長短,在任務代碼通知線程中斷之前可能需要一些時間(通過使用 Thread.isInterrupted()或通過調用阻塞方法輪詢中斷狀態)。如果任務需要更具響應性,則可以更頻繁地輪詢中斷狀態。阻止方法通常在進入時立即輪詢中斷狀態,InterruptedException 如果設置爲提高響應性則拋出 。

吞下一個中斷是可以接受的,當你知道線程即將退出時。這種情況只發生在調用可中斷方法的類是一個 Thread,而不是 Runnable 一般或通用庫代碼的一部分時,如清單5所示。它創建一個枚舉素數的線程,直到它被中斷並允許線程退出中斷。尋求主要的循環在兩個地方檢查中斷:一次是通過輪詢 isInterrupted() while 循環的頭部中的方法,一次是在調用阻塞 BlockingQueue.put() 方法時。

清單5.如果你知道線程即將退出,則可以吞下中斷

public class PrimeProducer extends Thread {    private final BlockingQueue<BigInteger> queue; 
    PrimeProducer(BlockingQueue<BigInteger> queue) {        this.queue = queue;
    }
 
    public void run() {        try {            BigInteger p = BigInteger.ONE;            while (!Thread.currentThread().isInterrupted())
                queue.put(p = p.nextProbablePrime());
        } catch (InterruptedException consumed) {            /* Allow thread to exit */
            /* 允許線程退出 */
        }
    }
 
    public void cancel() { interrupt(); }
}

不間斷阻塞

並非所有阻止方法都拋出 InterruptedException。輸入和輸出流類可能會阻止等待 I/O 完成,但它們不會拋出InterruptedException,並且如果它們被中斷,它們不會提前返回。但是,在套接字 I/O 的情況下,如果一個線程關閉了套接字,那麼阻塞其他線程中該套接字上的 I/O 操作將在早期完成SocketException。非阻塞 I/O 類 java.nio 也不支持可中斷 I/O,但可以通過關閉通道或請求喚醒來類似地取消阻塞操作 Selector。同樣,嘗試獲取內在鎖(輸入一個 synchronized 塊)不能被中斷,但 ReentrantLock 支持可中斷的採集模式。

不可取消的任務

有些任務只是拒絕被打斷,使它們無法取消。但是,即使是不可取消的任務也應該嘗試保留中斷狀態,以但在調用堆棧上層的代碼在非可取消任務完成後想要對發生的中斷進行響應。清單6顯示了一個等待阻塞隊列直到某個項可用的方法,無論它是否被中斷。爲了成爲一個好公民,它在完成後恢復最終塊中的中斷狀態,以免剝奪呼叫者的中斷請求。它無法提前恢復中斷狀態,因爲它會導致無限循環 - BlockingQueue.take(), 完成後則可以在進入時立即輪詢中斷狀態, 如果發現中斷狀態設置,則可以拋出InterruptedException。

清單6. 在返回之前恢復中斷狀態的非可執行任務

public Task getNextTask(BlockingQueue<Task> queue) {    boolean interrupted = false;    try {        while (true) {            try {                return queue.take();
            } catch (InterruptedException e) {
                interrupted = true;                // 失敗了再試
            }
        }
    } finally {        if (interrupted)
            Thread.currentThread().interrupt();
    }
}

摘要

你可以使用 Java 平臺提供的協作中斷機制來構建靈活的取消策略。作業可以決定它們是否可以取消,它們希望如何響應中斷,如果立即返回會影響應用程序的完整性,它們可以推遲中斷以執行特定於任務的清理。即使你想完全忽略代碼中斷,也要確保在捕獲 InterruptedException 並且不重新拋出代碼時恢復中斷狀態 ,以便調用它的代碼能夠發現中斷。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章