處理InterruptedException
這個故事可能很熟悉:你正在寫一個測試程序又憨,你需要暫停某個線程一段時間,所以你調用 Thread.sleep()屎即。然后編譯器或 IDE 就會抱怨說 InterruptedException 沒有拋出聲明或捕獲。什么是 InterruptedException,你為什么要處理它敲长?
最常見的響應 InterruptedException 做法是吞下它 - 捕獲它并且什么也不做(或者記錄它,也沒好多少) - 正如我們將在清單4中看到的那樣秉继。不幸的是祈噪,這種方法拋棄了關于??中斷發(fā)生的重要信息,這可能會損害應用程序取消活動或響應及時關閉的能力尚辑。
阻塞方法
當一個方法拋出 InterruptedException 時辑鲤,意味著幾件事情: 除了它可以拋出一個特定的檢查異常, 它還告訴你它是一種阻塞方法,它會嘗試解除阻塞并提前返回杠茬。
阻塞方法不同于僅需要很長時間才能運行完成的普通方法月褥。普通方法的完成僅取決于你要求它做多少事以及是否有足夠的計算資源(CPU周期和內存)。另一方面瓢喉,阻塞方法的完成還取決于某些外部事件宁赤,例如計時器到期,I/O 完成或另一個線程的操作(釋放鎖栓票,設置標志或放置任務到工作隊列)礁击。普通方法可以在完成工作后立即結束,但阻塞方法不太好預測逗载,因為它們依賴于外部事件哆窿。
因為如果他們正在等待永遠不會在事件,發(fā)生堵塞的方法有可能永遠不結束厉斟,常用在阻塞可取消的操作挚躯。對于長時間運行的非阻塞方法,通常也是可以取消的擦秽÷肜螅可取消操作是可以在通常自行完成之前從外部強制移動到完成狀態(tài)的操作。 Thread提供的Thread.sleep() 和 Object.wait() 方法中斷機制是一種取消線程繼續(xù)阻塞的機制; 它允許一個線程請求另一個線程提前停止它正在做的事情感挥。當一個方法拋出時 InterruptedException缩搅,它告訴你如果執(zhí)行方法的線程被中斷,它將嘗試停止它正在做的事情提前返回, 并通過拋出 InterruptedException 表明它的提早返回触幼。表現良好的阻塞庫方法應該響應中斷并拋出 InterruptedException 異常, 以便它們可以應用在可取消的活動中而不會妨礙程序的響應性硼瓣。
線程中斷
每個線程都有一個與之關聯的布爾屬性,表示其中斷狀態(tài)。中斷狀態(tài)最初為假; 當某個線程被其他線程通過調用中斷 Thread.interrupt() 時堂鲤, 會發(fā)生以下兩種情況之一: 如果該線程正在執(zhí)行低級別的中斷阻塞方法 Thread.sleep()亿傅,Thread.join()或 Object.wait()等,它取消阻塞并拋出 InterruptedException瘟栖。除此以外葵擎,interrupt() 僅設置線程的中斷狀態(tài)。在中斷的線程中運行的代碼可以稍后輪詢中斷的狀態(tài)以查看是否已經請求停止它正在做的事情; 中斷狀態(tài)可以通過 Thread.isInterrupted() 讀取半哟,并且可以在命名不佳的單個操作Thread.interrupted()中讀取和清除 酬滤。
中斷是一種合作機制。當一個線程中斷另一個線程時寓涨,被中斷的線程不一定會立即停止它正在做的事情敏晤。相反,中斷是一種禮貌地要求另一個線程在方便的時候停止它正在做什么的方式缅茉。有些方法嘴脾,比如Thread.sleep()認真對待這個請求,但方法不一定要注意中斷請求蔬墩。不阻塞但仍可能需要很長時間才能執(zhí)行完成的方法可以通過輪詢中斷狀態(tài)來尊重中斷請求译打,并在中斷時提前返回。你可以自由地忽略中斷請求拇颅,但這樣做可能會影響響應速度奏司。
中斷的合作性質的一個好處是它為安全地構建可取消的活動提供了更大的靈活性。我們很少想立即停止活動; 如果活動在更新期間被取消樟插,程序數據結構可能會處于不一致狀態(tài)韵洋。中斷允許可取消活動清理正在進行的任何工作,恢復不變量黄锤,通知其他活動取消事件搪缨,然后終止。
處理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奏篙,執(zhí)行清理柴淘,然后重新拋出異常。清單2是一種用于匹配在線游戲服務中的玩家的機制,說明了這種技術悠就。該 matchPlayers() 方法等待兩個玩家到達然后開始新游戲千绪。如果在一個玩家到達之后但在第二個玩家到達之前它被中斷充易,則在重新投擲之前將該玩家放回隊列 InterruptedException梗脾,以便玩家的游戲請求不會丟失。
清單2.在重新拋出 InterruptedException 之前執(zhí)行特定于任務的清理
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,它會清除中斷狀態(tài)改备。如果你抓住 InterruptedException 但不能重新拋出它控漠,你應該保留中斷發(fā)生的證據,以便調用堆棧上的代碼可以了解中斷并在需要時響應它悬钳。此任務通過調用 interrupt()實現“重新中斷”當前線程盐捷,如清單3所示。至少默勾,無論何時捕獲 InterruptedException 并且不重新拋出它碉渡,都要在返回之前重新中斷當前線程。
清單3.捕獲InterruptedException后恢復中斷狀態(tài)
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) {
//重要: 恢復中斷狀態(tài)
Thread.currentThread().interrupt();
}
}
}
你可以做的最糟糕的事情 InterruptedException 就是吞下它 - 抓住它母剥,既不重新拋出它也不重新確定線程的中斷狀態(tài)滞诺。處理你沒有規(guī)劃的異常的標準方法 - 捕獲它并記錄它 - 也算作吞噬中斷,因為調用堆棧上的代碼將無法找到它环疼。(記錄 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序愚,無論你是否計劃對中斷請求執(zhí)行操作,你仍然希望重新中斷當前線程等限,因為單個中斷請求可能有多個“收件人”爸吮。標準線程池(ThreadPoolExecutor)工作線程實現響應中斷,因此中斷線程池中運行的任務可能具有取消任務和通知執(zhí)行線程線程池正在關閉的效果望门。如果作業(yè)吞下中斷請求形娇,則工作線程可能不會知道請求了中斷,這可能會延遲應用程序或服務關閉筹误。
實施可取消的任務
語言規(guī)范中沒有任何內容給出任何特定語義的中斷桐早,但在較大的程序中,除了取消之外,很難保持中斷的任何語義哄酝。根據活動友存,用戶可以通過 GUI 或通過 JMX 或 Web 服務等網絡機制請求取消。它也可以由程序邏輯請求陶衅。例如屡立,如果 Web 爬蟲檢測到磁盤已滿,則可能會自動關閉自身搀军,或者并行算法可能會啟動多個線程來搜索解決方案空間的不同區(qū)域膨俐,并在其中一個找到解決方案后取消它們。
僅僅因為一個任務是取消并不意味著它需要一個中斷請求響應立即罩句。對于在循環(huán)中執(zhí)行代碼的任務焚刺,通常每次循環(huán)迭代僅檢查一次中斷。根據循環(huán)執(zhí)行的時間長短门烂,在任務代碼通知線程中斷之前可能需要一些時間(通過使用 Thread.isInterrupted()或通過調用阻塞方法輪詢中斷狀態(tài))乳愉。如果任務需要更具響應性,則可以更頻繁地輪詢中斷狀態(tài)屯远。阻止方法通常在進入時立即輪詢中斷狀態(tài)蔓姚,InterruptedException 如果設置為提高響應性則拋出 。
吞下一個中斷是可以接受的氓润,當你知道線程即將退出時赂乐。這種情況只發(fā)生在調用可中斷方法的類是一個 Thread,而不是 Runnable 一般或通用庫代碼的一部分時咖气,如清單5所示挨措。它創(chuàng)建一個枚舉素數的線程,直到它被中斷并允許線程退出中斷崩溪。尋求主要的循環(huán)在兩個地方檢查中斷:一次是通過輪詢 isInterrupted() while 循環(huán)的頭部中的方法浅役,一次是在調用阻塞 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 支持可中斷的采集模式端姚。
不可取消的任務
有些任務只是拒絕被打斷,使它們無法取消挤悉。但是渐裸,即使是不可取消的任務也應該嘗試保留中斷狀態(tài),以但在調用堆棧上層的代碼在非可取消任務完成后想要對發(fā)生的中斷進行響應尖啡。清單6顯示了一個等待阻塞隊列直到某個項可用的方法橄仆,無論它是否被中斷剩膘。為了成為一個好公民衅斩,它在完成后恢復最終塊中的中斷狀態(tài),以免剝奪呼叫者的中斷請求怠褐。它無法提前恢復中斷狀態(tài)畏梆,因為它會導致無限循環(huán) - BlockingQueue.take(), 完成后則可以在進入時立即輪詢中斷狀態(tài), 如果發(fā)現中斷狀態(tài)設置,則可以拋出InterruptedException奈懒。
清單6. 在返回之前恢復中斷狀態(tài)的非可執(zhí)行任務
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 平臺提供的協作中斷機制來構建靈活的取消策略奠涌。作業(yè)可以決定它們是否可以取消,它們希望如何響應中斷磷杏,如果立即返回會影響應用程序的完整性溜畅,它們可以推遲中斷以執(zhí)行特定于任務的清理。即使你想完全忽略代碼中斷极祸,也要確保在捕獲 InterruptedException 并且不重新拋出代碼時恢復中斷狀態(tài) 慈格,以便調用它的代碼能夠發(fā)現中斷。