並發王者課-青銅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用法。

延伸閱讀

關於作者

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

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