處理 InterruptedException

這樣的情景您也許並不陌生:您在編寫一個測試程序,程序須要暫停一段時間,因而調用Thread.sleep()。可是編譯器或 IDE 報錯說沒有處理檢查到的 InterruptedExceptionInterruptedException 是什麼呢,爲何必須處理它?html

對於 InterruptedException,一種常見的處理方式是 「生吞(swallow)」 它 —— 捕捉它,而後什麼也不作(或者記錄下它,不過這也好不到哪去)—— 就像後面的 清單 4 同樣。不幸的是,這種方法忽略了這樣一個事實:這期間可能發生中斷,而中斷可能致使應用程序喪失及時取消活動或關閉的能力。java

阻塞方法

當一個方法拋出 InterruptedException 時,它不只告訴您它能夠拋出一個特定的檢查異常,並且還告訴您其餘一些事情。例如,它告訴您它是一個阻塞(blocking)方法,若是您響應得當的話,它將嘗試消除阻塞並儘早返回。算法

阻塞方法不一樣於通常的要運行較長時間的方法。通常方法的完成只取決於它所要作的事情,以及是否有足夠多可用的計算資源(CPU 週期和內存)。而阻塞方法的完成還取決於一些外部的事件,例如計時器到期,I/O 完成,或者另外一個線程的動做(釋放一個鎖,設置一個標誌,或者將一個任務放在一個工做隊列中)。通常方法在它們的工做作完後便可結束,而阻塞方法較難於預測,由於它們取決於外部事件。阻塞方法可能影響響應能力,由於難於預測它們什麼時候會結束。安全

阻塞方法可能由於等不到所等的事件而沒法終止,所以令阻塞方法可取消 就很是有用(若是長時間運行的非阻塞方法是可取消的,那麼一般也很是有用)。可取消操做是指能從外部使之在正常完成以前終止的操做。由 Thread 提供並受 Thread.sleep() 和 Object.wait() 支持的中斷機制就是一種取消機制;它容許一個線程請求另外一個線程中止它正在作的事情。當一個方法拋出 InterruptedException 時,它是在告訴您,若是執行該方法的線程被中斷,它將嘗試中止它正在作的事情而提早返回,並經過拋出 InterruptedException 代表它提早返回。 行爲良好的阻塞庫方法應該能對中斷做出響應並拋出 InterruptedException,以便可以用於可取消活動中,而不至於影響響應。網絡

線程中斷

每一個線程都有一個與之相關聯的 Boolean 屬性,用於表示線程的中斷狀態(interrupted status)。中斷狀態初始時爲 false;當另外一個線程經過調用 Thread.interrupt() 中斷一個線程時,會出現如下兩種狀況之一。若是那個線程在執行一個低級可中斷阻塞方法,例如Thread.sleep()、 Thread.join() 或 Object.wait(),那麼它將取消阻塞並拋出 InterruptedException。不然, interrupt() 只是設置線程的中斷狀態。 在被中斷線程中運行的代碼之後能夠輪詢中斷狀態,看看它是否被請求中止正在作的事情。中斷狀態能夠經過Thread.isInterrupted() 來讀取,而且能夠經過一個名爲 Thread.interrupted() 的操做讀取和清除。數據結構

中斷是一種協做機制。當一個線程中斷另外一個線程時,被中斷的線程不必定要當即中止正在作的事情。相反,中斷是禮貌地請求另外一個線程在它願意而且方便的時候中止它正在作的事情。有些方法,例如 Thread.sleep(),很認真地對待這樣的請求,但每一個方法不是必定要對中斷做出響應。對於中斷請求,不阻塞可是仍然要花較長時間執行的方法能夠輪詢中斷狀態,並在被中斷的時候提早返回。 您能夠隨意忽略中斷請求,可是這樣作的話會影響響應。測試

中斷的協做特性所帶來的一個好處是,它爲安全地構造可取消活動提供更大的靈活性。咱們不多但願一個活動當即中止;若是活動在正在進行更新的時候被取消,那麼程序數據結構可能處於不一致狀態。中斷容許一個可取消活動來清理正在進行的工做,恢復不變量,通知其餘活動它要被取消,而後才終止。this

回頁首spa

處理 InterruptedException

若是拋出 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() throws InterruptedException { 
        try {
             Player playerOne, playerTwo;
             while (true) {
                 playerOne = playerTwo = null;
                 // Wait for two players to arrive and start a new game
                 playerOne = players.waitForPlayer(); // could throw IE
                 playerTwo = players.waitForPlayer(); // could throw IE
                 startNewGame(playerOne, playerTwo);
             }
         }
         catch (InterruptedException e) {  
             // If we got one player and were interrupted, put that player back
             if (playerOne != null)
                 players.addFirst(playerOne);
             // Then propagate the exception
             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) { 
             // Restore the interrupted status
             Thread.currentThread().interrupt();
         }
    }
}

處理 InterruptedException 時採起的最糟糕的作法是生吞它 —— 捕捉它,而後既不從新拋出它,也不從新斷言線程的中斷狀態。對於不知如何處理的異常,最標準的處理方法是捕捉它,而後記錄下它,可是這種方法仍然無異於生吞中斷,由於調用棧中更高層的代碼仍是沒法得到關於該異常的信息。(僅僅記錄 InterruptedException 也不是明智的作法,由於等到人來讀取日誌的時候,再來對它做出處理就爲時已晚了。) 清單 4 展現了一種使用得很普遍的模式,這也是生吞中斷的一種模式:

清單 4. 生吞中斷 —— 不要這麼作

// Don't do this 
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)worker 線程實現負責中斷,所以中斷一個運行在線程池中的任務能夠起到雙重效果,一是取消任務,二是通知執行線程線程池正要關閉。若是任務生吞中斷請求,則 worker 線程將不知道有一個被請求的中斷,從而耽誤應用程序或服務的關閉。

回頁首

實現可取消任務

語言規範中並無爲中斷提供特定的語義,可是在較大的程序中,難於維護除取消外的任何中斷語義。取決因而什麼活動,用戶能夠經過一個 GUI 或經過網絡機制,例如 JMX 或 Web 服務來請求取消。程序邏輯也能夠請求取消。例如,一個 Web 爬行器(crawler)若是檢測到磁盤已滿,它會自動關閉本身,不然一個並行算法會啓動多個線程來搜索解決方案空間的不一樣區域,一旦其中一個線程找到一個解決方案,就取消那些線程。

僅僅由於一個任務是可取消的,並不意味着須要當即 對中斷請求做出響應。對於執行一個循環中的代碼的任務,一般只需爲每個循環迭代檢查一次中斷。取決於循環執行的時間有多長,任何代碼可能要花一些時間才能注意到線程已經被中斷(或者是經過調用 Thread.isInterrupted() 方法輪詢中斷狀態,或者是調用一個阻塞方法)。 若是任務須要提升響應能力,那麼它能夠更頻繁地輪詢中斷狀態。阻塞方法一般在入口就當即輪詢中斷狀態,而且,若是它被設置來改善響應能力,那麼還會拋出 InterruptedException

唯一能夠生吞中斷的時候是您知道線程正要退出。只有當調用可中斷方法的類是 Thread 的一部分,而不是 Runnable 或通用庫代碼的狀況下,纔會發生這樣的場景,清單 5 演示了這種狀況。清單 5 建立一個線程,該線程列舉素數,直到被中斷,這裏還容許該線程在被中斷時退出。用於搜索素數的循環在兩個地方檢查是否有中斷:一處是在 while 循環的頭部輪詢 isInterrupted() 方法,另外一處是調用阻塞方法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 操做將提早結束,並拋出一個 SocketExceptionjava.nio 中的非阻塞 I/O 類也不支持可中斷 I/O,可是一樣能夠經過關閉通道或者請求 Selector 上的喚醒來取消阻塞操做。相似地,嘗試獲取一個內部鎖的操做(進入一個 synchronized 塊)是不能被中斷的,可是 ReentrantLock 支持可中斷的獲取模式。

不可取消的任務

有些任務拒絕被中斷,這使得它們是不可取消的。可是,即便是不可取消的任務也應該嘗試保留中斷狀態,以防在不可取消的任務結束以後,調用棧上更高層的代碼須要對中斷進行處理。清單 6 展現了一個方法,該方法等待一個阻塞隊列,直到隊列中出現一個可用項目,而無論它是否被中斷。爲了方便他人,它在結束後在一個 finally 塊中恢復中斷狀態,以避免剝奪中斷請求的調用者的權利。(它不能在更早的時候恢復中斷狀態,由於那將致使無限循環 —— 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;
                // fall through and retry
            }
        }
    } finally {
        if (interrupted)
            Thread.currentThread().interrupt();
    }
}

結束語

您能夠用 Java 平臺提供的協做中斷機制來構造靈活的取消策略。各活動能夠自行決定它們是可取消的仍是不可取消的,以及如何對中斷做出響應,若是當即返回會危害應用程序完整性的話,它們還能夠推遲中斷。即便您想在代碼中徹底忽略中斷,也應該確保在捕捉到 InterruptedException 可是沒有從新拋出它的狀況下,恢復中斷狀態,以避免調用它的代碼沒法獲知中斷的發生。

相關文章
相關標籤/搜索