並發王者課-青銅9:防患未然-如何處理執行緒中的異常

歡迎來到《並發王者課》,本文是該系列文章中的第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)

對於類似於空指針錯誤的堆棧資訊,相信你一定並不陌生。在主執行緒中處理這樣的異常很簡單,通過編寫trycatch程式碼塊即可。但其實,除了這種方式外,我們還可以通過定義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的用法。對於多執行緒的異常處理應該記住:

  • 執行緒內部的異常應儘可能在其內部解決
  • 如果主執行緒需要捕獲子執行緒異常,不可以使用trycatch,而是要使用uncaughtExceptionHandler。當然,已經在子執行緒內部捕獲的異常,主執行緒將無法捕獲。

正文到此結束,恭喜你又上了一顆星✨

夫子的試煉

  • 編寫程式碼了解並體驗uncaughtExceptionHandler用法。

延伸閱讀

關於作者

關注公眾號【庸人技術笑談】,獲取及時文章更新。記錄平凡人的技術故事,分享有品質(盡量)的技術文章,偶爾也聊聊生活和理想。不販賣焦慮,不做標題黨。

如果本文對你有幫助,歡迎點贊關注監督,我們一起從青銅到王者