Java-線程中的異常

1.嘗試使用外部線程捕獲子線程運行時錯誤

給出以下例子,我想問題是線程t1運行期間拋出的異常能夠被捕獲嗎?(這是一個相當好的問題~)

/**
 * @author Fisherman
 */

public class TempTest {

    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {

            System.out.println("我在1s後將拋出一個異常");

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            throw new RuntimeException();
        });

        try {
            t1.start();
        } catch (RuntimeException e) {
            e.printStackTrace();
        }
    }
}

控制檯輸出:

我在1s後將拋出一個異常
Exception in thread "Thread-0" java.lang.RuntimeException
	at concurrency_basic.TempTest.lambda$main$0(TempTest.java:21)
	at java.lang.Thread.run(Thread.java:748)

 可見這個異常沒有被捕獲。異常沒有被捕獲的原因是:因爲在main方法中執行完了t1.start();方法後很快返回了,所以很快就執行到了try語句塊外,甚至main線程直接就執行結束,在內存中先於線程t1被釋放了。第二個原因是start方法也不會一個拋出異常的方法,拋出異常的,也最多是t1線程對象的run方法。

 我們知道,如果我們對拋出的異常不做任何處理,那麼線程就會拋出異常後退出,不在執行拋出異常之後的語句。我們使用多線程的初衷即是將一個複雜的工作簡單化爲若干個小任務,一個線程的執行錯誤不應影響其他線程,線程是相互獨立的(不要想當然地任務寫在Main方法中的代碼都是屬於Main線程去的~)。

JVM的這種設計源自於這樣一種理念:“線程是獨立執行的代碼片斷,線程的問題應該由線程自己來解決,而不要委託到外部。

所以我們可以採取在對應線程的run方法中進行異常捕獲的處理,而不是委託給main線程:

public class TempTest {

    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {

            System.out.println("我在1s後將拋出一個異常");

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            try {
                throw new RuntimeException();
            } catch (RuntimeException e) {
                System.out.println("線程異常被捕獲了");
            }

        });

        t1.start();

    }

}

2.java.lang.Thread.UncaughtExceptionHandler

UncaughtExceptionHandlerThread類內部定義的一個抽象接口,其實現類是ThreadGroup

 下面我們來看看JDK對於此接口進行的描述:

Interface for handlers invoked when a Thread abruptly terminates due to an uncaught exception.
When a thread is about to terminate due to an uncaught exception the Java Virtual Machine will query the thread for its UncaughtExceptionHandler using getUncaughtExceptionHandler and will invoke the handler’s uncaughtException method, passing the thread and the exception as arguments. If a thread has not had its UncaughtExceptionHandler explicitly set, then its ThreadGroup object acts as its UncaughtExceptionHandler. If the ThreadGroup object has no special requirements for dealing with the exception, it can forward the invocation to the default uncaught exception handler.

 當一個線程因未捕獲的異常而即將終止時,JAVA虛擬機將使用Thread.getUncaughtExceptionHandler()查詢該線程以獲得其UncaughtExceptionHandler,並調用該handler的uncaughtException()方法,將線程和異常作爲參數傳遞。如果某一線程沒有明確設置其UncaughtExceptionHandler,則將他的ThreadGroup對象作爲其handler。如果ThreadGroup對象對異常沒有什麼特殊的要求,那麼ThreadGroup可以將調用轉發給默認的未捕獲異常處理器(即Thread類中定義的靜態的未捕獲異常處理器對象)。 所以,關於該接口的原理描述基本就那麼多了,直接上Thread類的源碼吧。

// 接口定義,規定了ThreadGroup接口只需要實現uncaughtException方法即可
public interface UncaughtExceptionHandler {  
    void uncaughtException(Thread t, Throwable e);  
}  
// 未捕獲異常實例屬性:未捕獲異常 
private volatile UncaughtExceptionHandler uncaughtExceptionHandler;  
// 未捕獲異常靜態屬性:默認未捕獲異常 
private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;  
// 設置默認的未捕獲異常 
public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) {  
    SecurityManager sm = System.getSecurityManager();  
    if (sm != null) {  
        sm.checkPermission(  
            new RuntimePermission("setDefaultUncaughtExceptionHandler")  
                );  
    }  
    defaultUncaughtExceptionHandler = eh;  
 }  
// 獲取默認未捕獲異常,這可能會在ThreadGroup的uncaughtException方法中被嘗試調用
public static UncaughtExceptionHandler getDefaultUncaughtExceptionHandler(){  
    return defaultUncaughtExceptionHandler;  
}  
// 獲取未捕獲異常 
public UncaughtExceptionHandler getUncaughtExceptionHandler() {  
    // 這裏纔是高潮,如果沒有設置未捕獲異常,那麼就將group屬性當作未捕獲異常 
    return uncaughtExceptionHandler != null ?  
        uncaughtExceptionHandler : group;  
}  
// 設置未捕獲異常 
public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) {  
    checkAccess();  
    uncaughtExceptionHandler = eh;  
}  

//

根據JDK文檔的描述,當出現未捕獲異常的時候,JVM調用的是Thread.getUncaughtExceptionHandler():

public UncaughtExceptionHandler getUncaughtExceptionHandler() {
    return uncaughtExceptionHandler != null ?
        uncaughtExceptionHandler : group;
}

 假如程序員沒有調用方法Thread.setUncaughtExceptionHandler來設置uncaughtExceptionHandler對象的引用,那麼將會將其值設置爲ThreadGroup group

public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) {  
    checkAccess();  
    uncaughtExceptionHandler = eh;  
}  

 Thread對象將ThreadGroup對象(ThreadGroup group 對象引用變量是每個Tread對象在構造時會進行初始化的)當作未捕獲異常處理器,而ThreadGroup實現了UncaughtExceptionHandler,所以轉到ThreadGroup的uncaughtException(Thread, Throwable)方法。

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)) {  
            System.err.print("Exception in thread \""  
                             + t.getName() + "\" ");  
            e.printStackTrace(System.err);  
        }  
    }  
}  

 關於Thread中ThreadGroup的設置,其實在Thread的所有構造函數中都會轉調init方法,其邏輯就是如果在實例化線程對象的時候沒有默認傳入ThreadGroup,那麼就會通過Thread.currentThread.getThreadGroup來得到線程組對象,main方法中有一個默認的main線程組,所以,即便你不傳入,還是會有一個默認的。

關於JVM是如何來啓動這個機制的方法,通過的是方法dispatchUncaughtException()來引發的:

/**
* Dispatch an uncaught exception to the handler. This method is
* intended to be called only by the JVM.
*/
private void dispatchUncaughtException(Throwable e) {
    getUncaughtExceptionHandler().uncaughtException(this, e);
}

 可見這個方法是隻能由JVM調用,其目的就是用於處理線程的異常。因爲我們沒有使用try-catch語句來包圍異常,所以這類運行時異常都被稱爲uncaught exception。由於傳入的線程對象爲this,所以之前的方法中入口參數Thread都是當前線程對象。

使用案例:

public class TempTest {

    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {

            System.out.println("我在1s後將拋出一個異常");

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            Thread.currentThread().setUncaughtExceptionHandler((t, e) -> {
                System.out.println("成功捕獲了線程:" + Thread.currentThread() + "的異常"+e.toString());
            });

            throw new RuntimeException("自定義的運行時異常");

        });

        t1.start();

    }

}

控制檯輸出:

我在1s後將拋出一個異常
成功捕獲了線程:Thread[Thread-0,5,main]的異常java.lang.RuntimeException: 自定義的運行時異常

 這一來,我們可以通過定義一個UncaufhtExceptionHandler就做到了處理線程中可能遇到的所有異常,這是比try-catch語句方便的地方,因爲可能由於線程過長,我們可能知道可能會出現異常的全部位置。

注意事項:

  1. 方法setUncaughtExceptionHandler() 的調用需要防止於可能拋出異常的代碼之前;

  2. 拋出異常之後的代碼不會如同使用try-catch對異常包圍那般,可以繼續運行;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章