如何在多线程环境中优雅的处理异常?

线程是我们日常开发工作中,经常用到的一种技术,通过利用多线程技术,来提高服务器资源的利用率,从而达到提高程序运行效率的目的。多线程作为应用程序与底层系统资源交互的一种手段,在高级语言中(类似java),线程相关的api比较少,而且多是偏向底层的native方法。很多线程相关的实现细节,作为上层应用开发者,是感知不到的。正常情况下,程序正常运行,我们无需关注过多细节,但是当程序出现了异常,而且是关于线程内的异常时,该怎么处理呢?近几天小编就遇到了关于线程的异常处理问题,在解决问题的过程中,对线程相关的异常处理机制,也有了更深的了解。

你是如何处理线程异常的?

多线程的开发范式,通常如下:

Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                // ....
            }
        });
        t.start();

将需要在线程中执行的业务逻辑,定义到实现了Runnable接口的的run方法中。
对于异常的处理,我们会在run方法中采用try{} catch(){} 的方式进行异常捕获和处理。

Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                try{
                    // ....
                } catch(Exception e) {
                    // ....
                }finally {
                    // ....
                }
            }
        });
        t.start();

但是结合上一篇 “你真的会处理异常吗” 中的介绍:有些场景下的异常处理,需要结合上下文,在产生异常的方法内,可能无法处理异常,产生的异常,需要让上层感知,在调用该方法的上层方法中进行异常的处理。

所以,子线程的run方法中产生异常时,我们并不能直接将其捕获。而且,我们也不可能在所有使用多线程的地方,对run方法上都添加try{}catch(){} 代码块,这样会让我们的代码变得很臃肿,除此之外,也不是所有的run方法都会抛出异常,需要我们来处理。

UncaughtExceptionHandler

那么当线程中产生了异常,而我们又没有对其进行捕获的情况下,异常会给谁来处理呢?

jdk设计之初,也考虑到了这个问题,通过 UncaughtExceptionHandler 来完善对线程异常的处理机制。

UncaughtExceptionHandler 定义如下:

@FunctionalInterface
    public interface UncaughtExceptionHandler {
        /**
         * Method invoked when the given thread terminates due to the
         * given uncaught exception.
         * <p>Any exception thrown by this method will be ignored by the
         * Java Virtual Machine.
         * @param t the thread
         * @param e the exception
         */
        void uncaughtException(Thread t, Throwable e);
    }

通过该接口的名称,可以大概知道它要做的事情:对没有被捕获的异常进行处理 。也就是说,如果在子线程中产生了异常,且没有捕获的情况下,异常会被 UncaughtExceptionHandler 进行处理。

UncaughtExceptionHandler如何捕获异常

其实UncaughtExceptionHandler本身是无法捕获异常的,它只是对被捕获到的异常进行处理,也就是异常的处理逻辑定义在UncaughtExceptionHandler中。

线程是一个操作系统层面的概念,java层面的Thread类,仅仅是对线程的一个封装,在Thread类中大部分方法都是native的,所以Thread抛出的异常,其实是系统级别线程抛出的异常,这个异常,java语言层面是无法捕获的。

UncaughtExceptionHandler的注释中,也大概告诉我们一些异常的捕获流程:

/**
     * Interface for handlers invoked when a <tt>Thread</tt> abruptly
     * terminates due to an uncaught exception.
     * <p>When a thread is about to terminate due to an uncaught exception
     * the Java Virtual Machine will query the thread for its
     * <tt>UncaughtExceptionHandler</tt> using
     * {@link #getUncaughtExceptionHandler} and will invoke the handler's
     * <tt>uncaughtException</tt> method, passing the thread and the
     * exception as arguments.
     * If a thread has not had its <tt>UncaughtExceptionHandler</tt>
     * explicitly set, then its <tt>ThreadGroup</tt> object acts as its
     * <tt>UncaughtExceptionHandler</tt>. If the <tt>ThreadGroup</tt> object
     * has no
     * special requirements for dealing with the exception, it can forward
     * the invocation to the {@linkplain #getDefaultUncaughtExceptionHandler
     * default uncaught exception handler}.
     *
     * @see #setDefaultUncaughtExceptionHandler
     * @see #setUncaughtExceptionHandler
     * @see ThreadGroup#uncaughtException
     * @since 1.5
     */

这段注释的意思大概是:当一个线程由于未捕获的异常,而将要终止时,jvm会捕获到这个异常,然后按照一定的规则找到一个处理该异常的 UncaughtExceptionHandler,并把产生异常的thread和异常实例,作为参数来调用 UncaughtExceptionHandler中的uncaughtException方法。

这里先做下小结:线程的常规异常处理方式:使用try{}catch(){}run方法包裹起来,有些场景下,是无法满足我们的需求。为了解决这个问题,jdk提供了UncaughtExceptionHandler机制来处理:对于run方法抛出的异常,会被jvm进行捕获,然后交由对应的UncaughtExceptionHandler来处理。最终实现了,线程内产生了异常,通过jvm捕获,然后分派到某个UncaughtExceptionHandler中来处理。

哪个 UncaughtExceptionHandler 来处理异常

jvm捕获到了异常,将异常分派给哪个UncaughtExceptionHandler呢?
在说分派流程前,我们先了解一下,线程中有哪些 UncaughtExceptionHandler,可以被使用:在Thread中有两个UncaughtExceptionHandler类型的变量:

// null unless explicitly set
    private volatile UncaughtExceptionHandler uncaughtExceptionHandler;

    // null unless explicitly set
    private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler; 

一个是 静态类型的变量 defaultUncaughtExceptionHandler,一个是实例变量 uncaughtExceptionHandler,静态类型的变量是对所有线程实例都生效的,而实例属性仅仅对某个Thread实例有效。

看到这两个变量,是不是有些陌生,平时使用多线程的时候,基本上很少用到过这两个变量,那UncaughtExceptionHandler 是如何生效的呢?

别急,这里要引出 线程组的概念了。

线程组

线程组,可以理解成一组线程的管理者,每个线程都有一个所属的组。使用线程组可以对组内的线程进行统一管理,如线程的停止,挂起等。其中最重要的是线程组自身实现了 UncaughtExceptionHandler接口,线程组内的线程可以使用所属的线程组,进行异常的异常处理。

线程组的一些核心代码如下:

public
class ThreadGroup implements Thread.UncaughtExceptionHandler {
    private final ThreadGroup parent;
    String name;
    int maxPriority;
    boolean destroyed;
    boolean daemon;
    boolean vmAllowSuspension;
    int nthreads;
    Thread threads[];// 线程组中的线程
    // ...

    /**
     * Creates an empty Thread group that is not in any Thread group.
     * This method is used to create the system Thread group.
     */
    private ThreadGroup() {     // called from C code
        this.name = "system";
        this.maxPriority = Thread.MAX_PRIORITY;
        this.parent = null;
    }
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);
            }
        }
    }

看到线程组是不是也有些陌生,没错,线程组这个类,平常也很少用到,创建线程的时候,也没有指定过线程组。那这个线程组,又是从哪里来的呢?

继续翻代码:

 public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
    
private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }
        this.name = name;
        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            /* Determine if it's an applet or not */

            /* If there is a security manager, ask the security manager
               what to do. */
            if (security != null) {
                g = security.getThreadGroup();
            }

            /* If the security doesn't have a strong opinion of the matter
               use the parent thread group. */
               //使用父线程的线程组
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }
       //...
        /* Set thread ID */
        tid = nextThreadID();
    }

初始化线程的时候,如果没有指定线程组的话,那么被初始化的线程的线程组就是"父线程"的线程组。

其实这里的父线程,仅仅是执行线程初始化方法的那个线程,和被创建的线程,并没有真正意义的父子关系。不先进程那样,子进程挂了,还有有父线程来回收。

那问题又来了,父线程的线程组,又是哪里来的? 按照上面说的父线程机制,可以逐层往上找,直到找到主线程,而主线程的线程组是jvm启动的时候指定的,这个涉及到了比较多的jvm底层知识,我们先不做深究,你知道主线程的线程组是一个名称为system的线程组就可以了。

所以到这里,一个线程的 uncaughtExceptionHandler 可以有三个来源,第一个是实例自身指定的第二个是defaultUncaughtExceptionHandler第三个是线程组

那这三个 uncaughtExceptionHandler 在处理异常时候的优先级是怎样的? 老规矩,看一下源码:

/**
     * 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捕获,jvm调用方法dispatchUncaughtException,来完成异常处理器的分派和异常的处理。具体分派逻辑如下:

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

这里可以看出:首先尝试使用 Thread实例的异常处理器,如果thread实例没有指定异常处理器的话,那么就使用线程组,作为异常处理器(线程组实现了 UncaughtExceptionHandler),

接下来,我们具体看一下线程组是如何处理异常的:

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);
            }
        }
    }

在线程组中,优先使用其父线程组进行异常的处理,线程组的父线程组类似于,线程的父线程一样,就是创建该线程组的线程的线程组,说起来比较绕,看一下代码就明白了:

    public ThreadGroup(String name) {
    	// 使用当前线程的线程组
        this(Thread.currentThread().getThreadGroup(), name);
    }
    
    public ThreadGroup(ThreadGroup parent, String name) {
        this(checkParentAccess(parent), parent, name);
    }

如果线程的线程组没有重写uncaughtException方法的话,那么这里找父线程组的逻辑就会层层递进,会找到主线程的线程组,主线程的线程组名称为 system,而system 线程组的父线程组为null,所以接下来尝试使用 defaultUncaughtExceptionHandler进行异常的处理,如果defaultUncaughtExceptionHandler线程也不存在的话,那么最后的处理逻辑,就是使用错误流,将异常信息输出。也就是我们经常在控制台看到的一大坨异常堆栈信息

总结

这里总结一下,在多线程环境中,如果没有使用try{}catch(){}显示的对异常进行捕获的话,线程的异常处理会交给UncaughtExceptionHandler 来完成。而在线程异常处理流程中存在三个 uncaughtExceptionHander:Thread实例变量uncaughtExceptionHander,Thrad的默认异常处理器defaultuncaughtExceptionHander和线程的线程组。这三个异常处理器在异常处理过程中的优先级如下:
Thread实例的 uncaughtExceptionHander > 父线程组 >defaultuncaughtExceptionHander

到这里,大概已经知道了 java中线程异常的处理方式。其实在工作中,我们使用多线程时,更多的是以线程池的方式使用,那么在使用线程池的时候,对异常的处理方式和线程的异常处理方式一样吗? 这个我们放在下一篇文章中学习。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值