對於InterruptedException,一種常見的處理方式是捕捉它,然後什麼也不做(或者記錄下它,不過這也好不到哪去)。不幸的是,這種方法忽略了這樣一個事實:這期間可能發生中斷,而中斷可能導致應用程式喪失及時取消活動或關閉的能力。
阻塞方法
當一個方法丟擲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(),很認真地對待這樣的請求,但每個方法不是一定要對中斷作出響應。對於中斷請求,不阻塞但是仍然要花較長時間執行的方法可以輪詢中斷狀態,並在被中斷的時候提前返回。您可以隨意忽略中斷請求,但是這樣做的話會影響響應。
中斷的協作特性所帶來的一個好處是,它為安全地構造可取消活動提供更大的靈活性。我們很少希望一個活動立即停止;如果活動在正在進行更新的時候被取消,那麼程式資料結構可能處於不一致狀態。中斷允許一個可取消活動來清理正在進行的工作,恢復不變數,通知其他活動它要被取消,然後才終止。
處理InterruptedException
如果丟擲InterruptedException意味著一個方法是阻塞方法,那麼呼叫一個阻塞方法則意味著您的方法也是一個阻塞方法,而且您應該有某種策略來處理InterruptedException。通常最容易的策略是自己丟擲InterruptedException,如清單1中putTask()和getTask()方法中的程式碼所示。這樣做可以使方法對中斷作出響應,並且只需將InterruptedException新增到throws子句。
import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; // 清單 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並且不重新丟擲它時,就在返回之前重新中斷當前執行緒。
import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeUnit; // 清單 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展示了一種使用得很廣泛的模式,這也是生吞中斷的一種模式:
import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeUnit; // 清單 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()。
import java.math.BigInteger; import java.util.concurrent.BlockingQueue; // 清單 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。java.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但是沒有重新丟擲它的情況下,恢復中斷狀態,以免呼叫它的程式碼無法獲知中斷的發生。
參考資料: