多線程中的異常處理


1.Thread的默認異常處理

線程不允許拋出未捕獲的checked exception(比如sleep時的InterruptedException),也就是說各個線程需要自己把自己的checked exception處理掉。我們可以查看一下Thread類的run()方法聲明,方法聲明上沒有對拋出異常進行任何約束。

    //Thread類中
    @Override
    public void run() {
        if (target != null) {
            target.run();//實際上直接調用Runnable實例的run方法
        }
    }
    
    //Runnable接口中
    public abstract void run();

JVM的這種設計源自於這樣一種理念:“線程是獨立執行的代碼片斷,線程的問題應該由線程自己來解決,而不要委托到外部。”基於這樣的設計理念,在Java中,線程方法的異常(無論是checked還是unchecked exception),都應該在線程代碼邊界之內(run方法內)進行try catch並處理掉。換句話說,我們不能捕獲從線程中逃逸的異常。

2.未捕獲的異常哪了?

一個異常被拋出后,如果沒有被捕獲處理,則會一直向上拋。異常一旦被Thread.run() 拋出后,就不能在程序中對異常進行捕獲,最終只能由JVM捕獲

    //不處理異常
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 1 / 0;//發生異常
            }
        }).start();
    }

//執行結果:
Exception in thread "Thread-0" java.lang.ArithmeticException: / by zero
    at thread.thread.thread.Uncaught$1.run
    at java.lang.Thread.run

======================================================
    //嘗試對異常進行捕獲
    public static void main(String[] args) {
        try {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    int i = 1 / 0;//發生異常
                }
            }).start();
        } catch (Exception e) {
            System.out.println("捕獲線程拋出的異常!");
        }
    }

//執行結果:
Exception in thread "Thread-0" java.lang.ArithmeticException: / by zero
    at thread.thread.thread.Uncaught$1.run
    at java.lang.Thread.run

上面的例子中,我們嘗試在main方法中對線程中拋出的異常進行捕獲,但是無濟於事。

3.JVM如何處理線程中拋出的異常

查看Thread類的源碼,我們可以看到有個dispatchUncaughtException方法,此方法就是用來處理線程中拋出的異常的。JVM會調用dispatchUncaughtException方法來尋找異常處理器(UncaughtExceptionHandler),處理異常。

    // 向handler分派未捕獲的異常。該方法僅由JVM調用。
    private void dispatchUncaughtException(Throwable e) {
        getUncaughtExceptionHandler().uncaughtException(this, e);
    }

    // 獲取用來處理未捕獲異常的handler,如果沒有設置則返回當前線程所屬的ThreadGroup
    public UncaughtExceptionHandler getUncaughtExceptionHandler() {
        return uncaughtExceptionHandler != null ?
            uncaughtExceptionHandler : group;
    }

UncaughtExceptionHandler必須顯示的設置,否則默認為null。若為null,則使用線程默認的handler,即該線程所屬的ThreadGroup。ThreadGroup自身就是一個handler,查看ThreadGroup的源碼就可以發現,ThreadGroup實現了Thread.UncaughtExceptionHandler接口,並實現了默認的處理方法。默認的未捕獲異常處理器處理時,會調用 System.err 進行輸出,也就是直接打印到控制台了。

    public void uncaughtException(Thread t, Throwable e) {
        if (parent != null) { // 父級優先處理
            parent.uncaughtException(t, e);
        } else {
            Thread.UncaughtExceptionHandler ueh = Thread.getDefaultUncaughtExceptionHandler();
            if (ueh != null) {
                ueh.uncaughtException(t, e);
            } else if (!(e instanceof ThreadDeath)) { // 沒有配置handler時,默認直接打印到控制台
                System.err.print("Exception in thread \""
                                 + t.getName() + "\" ");
                e.printStackTrace(System.err);
            }
        }
    }

由此可知,最終JVM是調用未捕獲的異常處理器的uncaughtException()方法來處理異常的,並且是直接打印到控制台。

4.如何自定義處理異常

那么,如果我們要自己處理異常,該怎么辦呢?通過前面的分析,我們已經知道了線程會使用默認的未捕獲異常處理器來處理異常。自然我們可以想到,是否可以自定義未捕獲異常處理器,覆蓋掉默認的捕獲異常處理器。實際上,Thead確實已經提供了一個setUncaughtExceptionHandler方法,我們只需要將自定義未捕獲異常處理器作為參數傳入進入就可以了。

    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 1 / 0;//發生異常
            }
        });
        //自定義未捕獲異常處理器
        thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread t, Throwable e) {
                System.out.println("嘗試捕獲線程拋出的異常!");
            }
        });
        thread.start();
    }
    
//執行結果:
嘗試捕獲線程拋出的異常!

5.線程池中自定義處理異常

要自定義處理異常,只需要為線程提供一個自定義的UncaughtExceptionHandler。而在線程池中,該如何批量的為所有線程設置UncaughtExceptionHandler呢?我們知道,線程池中的線程是由線程工廠創建的。我們可以跟蹤ThreadPoolExecutor構造方法的源碼,最終定位到DefaultThreadFactory類,該類中有個newThread()方法,這就是線程產生的源頭了。

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }
    
    //Executors類中
    public static ThreadFactory defaultThreadFactory() {
        return new DefaultThreadFactory();
    }
    
    //DefaultThreadFactory類中
    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r,
                              namePrefix + threadNumber.getAndIncrement(),
                              0);
        if (t.isDaemon())
            t.setDaemon(false);
        if (t.getPriority() != Thread.NORM_PRIORITY)
            t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }

找到了線程創建的源頭,一切就OK了。我們可以實現一個自己的線程工廠,並覆蓋默認的newThread方法,為新創建的線程提供一個UncaughtExceptionHandler,即可達到我們的目的。由於DefaultThreadFactory是Executors類的內部類,我們不能直接繼承該類,只能實現該工廠類的接口——ThreadFactory接口,來實現我們自己的線程工廠。

class MyThreadFactory implements ThreadFactory {

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread();
        //自定義UncaughtExceptionHandler
        t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
        return t;
    }

}

需要注意的是,只有通過execute()方法提交任務,才能將它拋出的異常交給未捕獲異常處理器。而通過submit()方法提交的任務,無論是拋出未檢查異常還是已檢查異常,都將被認為是任務返回狀態的一部分。如果一個由submit()方法提交的任務由於拋出了異常而結束,該異常將被Future.get()封裝在ExecutionException中重新拋出。 

 

參考:

多線程異常處理 

線程的異常處理機制


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM