Java中斷異常的正確處理方法
處理InterruptedException
這個故事可能很熟悉:你正在寫一個測試程序,你需要暫停某個線程一段時間,所以你調(diào)用 Thread.sleep()。然后編譯器或 IDE 就會抱怨說 InterruptedException 沒有拋出聲明或捕獲。什么是 InterruptedException,你為什么要處理它?
最常見的響應(yīng) InterruptedException 做法是吞下它 - 捕獲它并且什么也不做(或者記錄它,也沒好多少) - 正如我們將在清單4中看到的那樣。不幸的是,這種方法拋棄了關(guān)于中斷發(fā)生的重要信息,這可能會損害應(yīng)用程序取消活動或響應(yīng)及時關(guān)閉的能力。
阻塞方法
當一個方法拋出 InterruptedException 時,意味著幾件事情: 除了它可以拋出一個特定的檢查異常, 它還告訴你它是一種阻塞方法,它會嘗試解除阻塞并提前返回。
阻塞方法不同于僅需要很長時間才能運行完成的普通方法。普通方法的完成僅取決于你要求它做多少事以及是否有足夠的計算資源(CPU周期和內(nèi)存)。另一方面,阻塞方法的完成還取決于某些外部事件,例如計時器到期,I/O 完成或另一個線程的操作(釋放鎖,設(shè)置標志或放置任務(wù)到工作隊列)。普通方法可以在完成工作后立即結(jié)束,但阻塞方法不太好預(yù)測,因為它們依賴于外部事件。
因為如果他們正在等待永遠不會在事件,發(fā)生堵塞的方法有可能永遠不結(jié)束,常用在阻塞可取消的操作。對于長時間運行的非阻塞方法,通常也是可以取消的??扇∠僮魇强梢栽谕ǔW孕型瓿芍皬耐獠繌娭埔苿拥酵瓿蔂顟B(tài)的操作。 Thread提供的Thread.sleep() 和 Object.wait() 方法中斷機制是一種取消線程繼續(xù)阻塞的機制; 它允許一個線程請求另一個線程提前停止它正在做的事情。當一個方法拋出時 InterruptedException,它告訴你如果執(zhí)行方法的線程被中斷,它將嘗試停止它正在做的事情提前返回, 并通過拋出 InterruptedException 表明它的提早返回。表現(xiàn)良好的阻塞庫方法應(yīng)該響應(yīng)中斷并拋出 InterruptedException 異常, 以便它們可以應(yīng)用在可取消的活動中而不會妨礙程序的響應(yīng)性。
線程中斷
每個線程都有一個與之關(guān)聯(lián)的布爾屬性,表示其中斷狀態(tài)。中斷狀態(tài)最初為假; 當某個線程被其他線程通過調(diào)用中斷 Thread.interrupt() 時, 會發(fā)生以下兩種情況之一: 如果該線程正在執(zhí)行低級別的中斷阻塞方法 Thread.sleep(),Thread.join()或 Object.wait()等,它取消阻塞并拋出 InterruptedException。除此以外,interrupt() 僅設(shè)置線程的中斷狀態(tài)。在中斷的線程中運行的代碼可以稍后輪詢中斷的狀態(tài)以查看是否已經(jīng)請求停止它正在做的事情; 中斷狀態(tài)可以通過 Thread.isInterrupted() 讀取,并且可以在命名不佳的單個操作Thread.interrupted()中讀取和清除 。
中斷是一種合作機制。當一個線程中斷另一個線程時,被中斷的線程不一定會立即停止它正在做的事情。相反,中斷是一種禮貌地要求另一個線程在方便的時候停止它正在做什么的方式。有些方法,比如Thread.sleep()認真對待這個請求,但方法不一定要注意中斷請求。不阻塞但仍可能需要很長時間才能執(zhí)行完成的方法可以通過輪詢中斷狀態(tài)來尊重中斷請求,并在中斷時提前返回。你可以自由地忽略中斷請求,但這樣做可能會影響響應(yīng)速度。
中斷的合作性質(zhì)的一個好處是它為安全地構(gòu)建可取消的活動提供了更大的靈活性。我們很少想立即停止活動; 如果活動在更新期間被取消,程序數(shù)據(jù)結(jié)構(gòu)可能會處于不一致狀態(tài)。中斷允許可取消活動清理正在進行的任何工作,恢復(fù)不變量,通知其他活動取消事件,然后終止。
處理InterruptedException
如果 throw InterruptedException 意味著這個方法是一個阻塞方法,那么調(diào)用一個阻塞方法意味著你的方法也是一個阻塞方法,你應(yīng)該有一個處理策略 InterruptedException。通常最簡單的策略是你自己也拋出 InterruptedException 異常,如清單1 中的 putTask() 和 getTask() 方法所示。這樣做會使你的方法響應(yīng)中斷,并且通常只需要添加 InterruptedException 到 throws 子句。
清單1.通過不捕獲它來向調(diào)用者傳播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是一種用于匹配在線游戲服務(wù)中的玩家的機制,說明了這種技術(shù)。該 matchPlayers() 方法等待兩個玩家到達然后開始新游戲。如果在一個玩家到達之后但在第二個玩家到達之前它被中斷,則在重新投擲之前將該玩家放回隊列 InterruptedException,以便玩家的游戲請求不會丟失。
清單2.在重新拋出 InterruptedException 之前執(zhí)行特定于任務(wù)的清理
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 調(diào)用可中斷方法定義的任務(wù)時。在這種情況下,你不能重新拋出 InterruptedException,但你也不想做任何事情。當阻塞方法檢測到中斷和拋出時 InterruptedException,它會清除中斷狀態(tài)。如果你抓住 InterruptedException 但不能重新拋出它,你應(yīng)該保留中斷發(fā)生的證據(jù),以便調(diào)用堆棧上的代碼可以了解中斷并在需要時響應(yīng)它。此任務(wù)通過調(diào)用 interrupt()實現(xiàn)“重新中斷”當前線程,如清單3所示。至少,無論何時捕獲 InterruptedException 并且不重新拋出它,都要在返回之前重新中斷當前線程。
清單3.捕獲InterruptedException后恢復(fù)中斷狀態(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) { //重要: 恢復(fù)中斷狀態(tài) Thread.currentThread().interrupt(); } } }
你可以做的最糟糕的事情 InterruptedException 就是吞下它 - 抓住它,既不重新拋出它也不重新確定線程的中斷狀態(tài)。處理你沒有規(guī)劃的異常的標準方法 - 捕獲它并記錄它 - 也算作吞噬中斷,因為調(diào)用堆棧上的代碼將無法找到它。(記錄 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)工作線程實現(xiàn)響應(yīng)中斷,因此中斷線程池中運行的任務(wù)可能具有取消任務(wù)和通知執(zhí)行線程線程池正在關(guān)閉的效果。如果作業(yè)吞下中斷請求,則工作線程可能不會知道請求了中斷,這可能會延遲應(yīng)用程序或服務(wù)關(guān)閉。
實施可取消的任務(wù)
語言規(guī)范中沒有任何內(nèi)容給出任何特定語義的中斷,但在較大的程序中,除了取消之外,很難保持中斷的任何語義。根據(jù)活動,用戶可以通過 GUI 或通過 JMX 或 Web 服務(wù)等網(wǎng)絡(luò)機制請求取消。它也可以由程序邏輯請求。例如,如果 Web 爬蟲檢測到磁盤已滿,則可能會自動關(guān)閉自身,或者并行算法可能會啟動多個線程來搜索解決方案空間的不同區(qū)域,并在其中一個找到解決方案后取消它們。
僅僅因為一個任務(wù)是取消并不意味著它需要一個中斷請求響應(yīng)立即。對于在循環(huán)中執(zhí)行代碼的任務(wù),通常每次循環(huán)迭代僅檢查一次中斷。根據(jù)循環(huán)執(zhí)行的時間長短,在任務(wù)代碼通知線程中斷之前可能需要一些時間(通過使用 Thread.isInterrupted()或通過調(diào)用阻塞方法輪詢中斷狀態(tài))。如果任務(wù)需要更具響應(yīng)性,則可以更頻繁地輪詢中斷狀態(tài)。阻止方法通常在進入時立即輪詢中斷狀態(tài),InterruptedException 如果設(shè)置為提高響應(yīng)性則拋出 。
吞下一個中斷是可以接受的,當你知道線程即將退出時。這種情況只發(fā)生在調(diào)用可中斷方法的類是一個 Thread,而不是 Runnable 一般或通用庫代碼的一部分時,如清單5所示。它創(chuàng)建一個枚舉素數(shù)的線程,直到它被中斷并允許線程退出中斷。尋求主要的循環(huán)在兩個地方檢查中斷:一次是通過輪詢 isInterrupted() while 循環(huán)的頭部中的方法,一次是在調(diào)用阻塞 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 的情況下,如果一個線程關(guān)閉了套接字,那么阻塞其他線程中該套接字上的 I/O 操作將在早期完成SocketException。非阻塞 I/O 類 java.nio 也不支持可中斷 I/O,但可以通過關(guān)閉通道或請求喚醒來類似地取消阻塞操作 Selector。同樣,嘗試獲取內(nèi)在鎖(輸入一個 synchronized 塊)不能被中斷,但 ReentrantLock 支持可中斷的采集模式。
不可取消的任務(wù)
有些任務(wù)只是拒絕被打斷,使它們無法取消。但是,即使是不可取消的任務(wù)也應(yīng)該嘗試保留中斷狀態(tài),以但在調(diào)用堆棧上層的代碼在非可取消任務(wù)完成后想要對發(fā)生的中斷進行響應(yīng)。清單6顯示了一個等待阻塞隊列直到某個項可用的方法,無論它是否被中斷。為了成為一個好公民,它在完成后恢復(fù)最終塊中的中斷狀態(tài),以免剝奪呼叫者的中斷請求。它無法提前恢復(fù)中斷狀態(tài),因為它會導(dǎo)致無限循環(huán) - BlockingQueue.take(), 完成后則可以在進入時立即輪詢中斷狀態(tài), 如果發(fā)現(xiàn)中斷狀態(tài)設(shè)置,則可以拋出InterruptedException。
清單6. 在返回之前恢復(fù)中斷狀態(tài)的非可執(zhí)行任務(wù)
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 平臺提供的協(xié)作中斷機制來構(gòu)建靈活的取消策略。作業(yè)可以決定它們是否可以取消,它們希望如何響應(yīng)中斷,如果立即返回會影響應(yīng)用程序的完整性,它們可以推遲中斷以執(zhí)行特定于任務(wù)的清理。即使你想完全忽略代碼中斷,也要確保在捕獲 InterruptedException 并且不重新拋出代碼時恢復(fù)中斷狀態(tài) ,以便調(diào)用它的代碼能夠發(fā)現(xiàn)中斷。
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,謝謝大家對腳本之家的支持。
相關(guān)文章
去掉IntelliJ IDEA 中 mybatis 對應(yīng)的 xml 文件警告的教程圖解
本文通過圖文并茂的形式給大家介紹了去掉IntelliJ IDEA 中 mybatis 對應(yīng)的 xml 文件警告的教程,需要的朋友可以參考下2018-06-06