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

處理InterruptedException

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

最多見的響應 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()認真對待這個請求,但方法不必定要注意中斷請求。不阻塞但仍可能須要很長時間才能執行完成的方法能夠經過輪詢中斷狀態來尊重中斷請求,並在中斷時提早返回。你能夠自由地忽略中斷請求,但這樣作可能會影響響應速度。this

中斷的合做性質的一個好處是它爲安全地構建可取消的活動提供了更大的靈活性。咱們不多想當即中止活動; 若是活動在更新期間被取消,程序數據結構可能會處於不一致狀態。中斷容許可取消活動清理正在進行的任何工做,恢復不變量,通知其餘活動取消事件,而後終止。線程

處理InterruptedException

若是 throw InterruptedException 意味着這個方法是一個阻塞方法,那麼調用一個阻塞方法意味着你的方法也是一個阻塞方法,你應該有一個處理策略 InterruptedException。一般最簡單的策略是你本身也拋出 InterruptedException 異常,如清單1 中的 putTask() 和 getTask() 方法所示。這樣作會使你的方法響應中斷,而且一般只須要添加 InterruptedException 到 throws 子句。日誌

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

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 而且不從新拋出代碼時恢復中斷狀態 ,以便調用它的代碼可以發現中斷。

相關文章
相關標籤/搜索