歡迎來到《併發王者課》,本文是該系列文章中的第9篇。
在本篇文章中,我將為你介紹執行緒中異常的處理方式以及uncaughtExceptionHandler用法。
一、新執行緒中的異常去哪了
應用程式在執行過程中,難免會出現各種意外錯誤,如果我們沒有對錯誤進行捕獲處理,會直接影響應用的執行結果,甚至導致應用崩潰。而在應用異常處理中,多執行緒的異常處理是比較重要又容易犯錯的地方。
接下來,我們通過一段程式碼模擬一種常見的多執行緒異常處理方式。
在下面的程式碼中,我們在主執行緒中建立了新執行緒nezhaThread,並期望在主執行緒中捕獲新執行緒中丟擲的異常:
public static void main(String[] args) {
Thread neZhaThread = new Thread() {
public void run() {
throw new RuntimeException("我是哪吒,我被圍攻了!");
}
};
// 嘗試捕獲執行緒丟擲的異常
try {
neZhaThread.start();
} catch (Exception e) {
System.out.println("接收英雄異常:" + e.getMessage());
}
}
執行結果如下:
Exception in thread "Thread-0" java.lang.RuntimeException: 我是哪吒,我被圍攻了!
at cn.tao.king.juc.execises1.ExceptionDemo$1.run(ExceptionDemo.java:7)
Process finished with exit code 0
對於多執行緒新手來說,可能並不能直接看出其中的不合理。然而,從執行的結果中可以看到,沒有輸出“接收英雄異常”關鍵字。也就是說,主執行緒並未能捕獲新執行緒的異常。 那這是為什麼呢?
理解這一現象,首先要從執行緒的本質出發。在Java中,每個執行緒所執行的都是獨立執行的程式碼片段,如果我們沒有主動提供執行緒間通訊和協作的機制,那麼它們彼此之間是隔離的。
換句話說,每個執行緒都要在自己的閉環內完成全部的任務處理,包括對異常的處理,如果出錯了但你沒有主動處理異常,那麼它們會按照既定的流程自我了結。
二、多執行緒中的異常處理方式
1. 從主執行緒看異常的處理
為了理解多執行緒中的錯誤處理方式,我們先看常見的主執行緒是如何處理錯誤的,畢竟相對於多執行緒,單一的主執行緒更容易讓人理解。
public static void main(String[] args) {
throw new NullPointerException();
}
很明顯,上面這段程式碼將會丟擲下面錯誤資訊:
Exception in thread "main" java.lang.NullPointerException
at cn.tao.king.juc.execises1.ExceptionDemo.main(ExceptionDemo.java:21)
對於類似於空指標錯誤的堆疊資訊,相信你一定並不陌生。在主執行緒中處理這樣的異常很簡單,通過編寫try
、catch
程式碼塊即可。但其實,除了這種方式外,我們還可以通過定義uncaughtExceptionHandler
來處理主執行緒中的異常。
public static void main(String[] args) {
Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
throw new NullPointerException();
}
// 自定義錯誤處理
static class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
public void uncaughtException(Thread t, Throwable e) {
System.out.println("出錯了!執行緒名:" + t.getName() + ",錯誤資訊:" + e.getMessage());
}
}
輸出結果如下:
出錯了!執行緒名:main,錯誤資訊:null
Process finished with exit code 1
你看,我們已經地捕獲了異常。然而,你可能會疑惑為什麼Thread.UncaughtExceptionHandler可以自定義錯誤處理?說到這,就不得不提Java中的異常處理方式,如下圖所示:
在Java中,我們經常可以看到空指標那樣的錯誤的堆疊資訊,然而這個堆疊實則是執行緒在出錯的情況下 “不得已” 才輸出來的。從圖中我們可以看到:
- 當執行緒出錯時,首先會檢查當前執行緒是否指定了錯誤處理器;
- 如果當前執行緒沒有指定錯誤處理器,則繼續檢查其所在的執行緒組是否指定(注意,前面我們已經說過,每個執行緒都是有執行緒組的);
- 如果當前執行緒的執行緒組也沒有指定,則繼續檢查其父執行緒是否指定;
- 如果父執行緒同樣沒有指定錯誤處理器,則最後檢查預設處理是否設定;
- 如果預設處理器也沒有設定,那麼將不得不輸出錯誤的堆疊資訊。
2. 多執行緒間的異常處理
不要忘記,主執行緒也是執行緒,所以當你理解了主執行緒的錯誤處理方式後,你也就理解了子執行緒中的異常處理方式,它們和主執行緒是相同的。在主執行緒中,我們可以通過Thread.setDefaultUncaughtExceptionHandler
來設定自定義異常處理器。而在新的子執行緒中,則可以通過執行緒物件直接指定異常處理器,比如我們給前面的neZhaThread執行緒設定異常處理器:
neZhaThread.setName("哪吒");
neZhaThread.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
那麼,設定處理器後的執行緒異常資訊則輸出如下:
出錯了!執行緒名:哪吒,錯誤資訊:我是哪吒,我被圍攻了!
Process finished with exit code 0
你看,通過定義uncaughtExceptionHandler
,我們已經捕獲並處理了新執行緒丟擲的異常。
3. 理解UncaughtExceptionHandler
從上面的程式碼中,相信你已經直觀地理解UncaughtExceptionHandler用法。在Java中,UncaughtExceptionHandler用於處理執行緒突然異常終止的情況。當執行緒因某種原因丟擲未處理的異常時,JVM虛擬機器將會通過執行緒中的getUncaughtExceptionHandler
查詢該執行緒的錯誤處理器,並將該執行緒和異常資訊作為引數傳遞過去。如果該執行緒沒有指定錯誤處理器,將會按照上圖所示的流程繼續查詢。
三、 定義uncaughtExceptionHandler的三個層面
1. 定義預設異常處理器
預設的錯誤處理器可以作為執行緒異常的兜底處理器,線上程和執行緒組未指定異常處理器時,可以使用預設的異常處理器。
Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
2. 自定義特定的異常處理器
如果某個執行緒需要特定的處理器時,通過執行緒物件指定異常處理器是個很不錯的選擇。當然,這種異常處理器不可以與其他執行緒共享。
neZhaThread.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
3. 繼承ThreadGroup
通過繼承ThreadGroup並覆寫uncaughtException
可以重設當前執行緒組的異常處理器邏輯。不過要注意的是,覆寫執行緒組的行為並不常見,使用時需要慎重。
public class MyThreadGroupDemo extends ThreadGroup{
public MyThreadGroupDemo(String name) {
super(name);
}
@Override
public void uncaughtException(Thread t, Throwable e) {
// 在這裡重寫執行緒組的異常處理邏輯
System.out.println("出錯了!執行緒名:" + t.getName() + ",錯誤資訊:" + e.getMessage());
}
}
小結
以上就是關於執行緒異常處理的全部內容,在本文中我們介紹了多執行緒異常的處理方式以及uncaughtExceptionHandler
的用法。對於多執行緒的異常處理應該記住:
- 執行緒內部的異常應儘可能在其內部解決;
- 如果主執行緒需要捕獲子執行緒異常,不可以使用
try
、catch
,而是要使用uncaughtExceptionHandler
。當然,已經在子執行緒內部捕獲的異常,主執行緒將無法捕獲。
正文到此結束,恭喜你又上了一顆星✨
夫子的試煉
- 編寫程式碼瞭解並體驗uncaughtExceptionHandler用法。
延伸閱讀
關於作者
關注公眾號【庸人技術笑談】,獲取及時文章更新。記錄平凡人的技術故事,分享有品質(儘量)的技術文章,偶爾也聊聊生活和理想。不販賣焦慮,不做標題黨。
如果本文對你有幫助,歡迎點贊、關注、監督,我們一起從青銅到王者。