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对异常包围那般,可以继续运行;

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