【线程基础】【七】UncaughtExceptionHandler 的使用

1  前言

我们平时在 Java 中处理异常的时候,通常的做法是使用try-catch-finally来包含代码块,但是Java自身还有一种方式可以处理就是使用UncaughtExceptionHandler,本节我们就来看看。

2  UncaughtExceptionHandler

2.1  认识

当 JVM 检测出某个线程由于未捕获的异常而终结的情况时,JVM会把这个事件报告给应用程序提供的UncaughtExceptionHandler异常处理器。这个是 JDK1.5 开始出现的,它是位于 Thread 类里的一个内部接口:

@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:

// 给某个线程单独设置异常处理器
thread.setUncaughtExceptionHandler(UncaughtExceptionHandler handler);

或者通过以下静态方法来设置全局默认的UncaughtExceptionHandler:

// 全局的默认异常处理器
Thread.setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler handler);

这些异常处理器中,只有一个将会被调用——JVM首先搜索每个线程的异常处理器,若没有,则搜索该线程的ThreadGroup的异常处理器。ThreadGroup中的默认异常处理器实现是将处理工作逐层委托给上层的ThreadGroup,直到某个ThreadGroup的异常处理器能够处理该异常,否则一直传递到顶层的ThreadGroup。顶层ThreadGroup的异常处理器委托给默认的系统处理器(如果默认的处理器存在,默认情况下为空),否则把栈信息输出到System.err。

2.2  简单实践

下面我们来尝试一个:

/**
 * @author: kuku
 * @description
 */
public class UncaughtTest {

    public static void main(String[] args) {
        // 创建一个线程
        Thread thread = new Thread(() -> {
            // 这里直接抛一个错
            throw new RuntimeException("出错了");
        });
        // 设置它的异常处理器
        thread.setUncaughtExceptionHandler((Thread t, Throwable e) -> {
            System.out.println(String.format("%s:发生了异常,e=%s", t.getName(), e.getMessage()));
        });
        // 启动线程
        thread.start();
    }
}

可以看到异常被异常处理器进行处理了:

我们再来个全局默认的和单独自己的:

/**
 * @author: kuku
 * @description
 */
public class UncaughtTest {

    public static void main(String[] args) {
        Thread.setDefaultUncaughtExceptionHandler((Thread t, Throwable e) -> {
            System.out.println(String.format("我是全局默认的处理器,t=%s,e=%s", t.getName(), e.getMessage()));
        });
        // 创建一个线程
        Thread thread = new Thread(() -> {
            // 这里直接抛一个错
            throw new RuntimeException("出错了");
        });
        // 设置它自己的异常处理器
        thread.setUncaughtExceptionHandler((Thread t, Throwable e) -> {
            System.out.println(String.format("自己的异常处理器%s:发生了异常,e=%s", t.getName(), e.getMessage()));
        });
        // 启动线程
        thread.start();
        // main 线程也抛一个异常
        throw new RuntimeException("main抛错");
    }
}

main 线程没有自己的异常处理器,就走线程默认的,Thread-0有自己的就用自己的(就近原则):

2.3  搭配线程池

我们平时使用线程基本都是线程池,那么它怎么跟我们的线程池搭配使用呢?就在我们线程池的 ThreadFactory里可以引进:

/**
 * @author: kuku
 * @description
 */
public class UncaughtTest {

    // 创建一个线程池
    private static final ThreadPoolExecutor POOL_EXECUTOR = new ThreadPoolExecutor(
        10,
        10,
        60,
        TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(1000),
        new MyThreadFactory((Thread t, Throwable e) ->{
            System.out.println(String.format("%s:发生了异常,e=%s", t.getName(), e.getMessage()));
        })
    );

    private static class MyThreadFactory implements ThreadFactory {

        private Thread.UncaughtExceptionHandler uncaughtExceptionHandler;

        MyThreadFactory(Thread.UncaughtExceptionHandler uncaughtExceptionHandler) {
            this.uncaughtExceptionHandler = uncaughtExceptionHandler;
        }

        @Override
        public Thread newThread(Runnable r) {
            Thread thread = new Thread(r);
            // 设置异常处理器
            thread.setUncaughtExceptionHandler(uncaughtExceptionHandler);
            return thread;
        }
    }

    public static void main(String[] args) {
        POOL_EXECUTOR.execute(() -> {throw new RuntimeException("1");});
        POOL_EXECUTOR.execute(() -> {throw new RuntimeException("2");});
        POOL_EXECUTOR.execute(() -> {throw new RuntimeException("3");});
        POOL_EXECUTOR.execute(() -> {throw new RuntimeException("4");});
    }
}

看效果:

这里线程池执行任务,我用的 execute,那当我换成 submit 还会达到同样的效果么?

public static void main(String[] args) throws ExecutionException, InterruptedException {
    Future<Object> one = POOL_EXECUTOR.submit(() -> {
        throw new RuntimeException("1");
    });
    System.out.println(one.get());
}

我们看执行结果告诉我们,get的时候遇到了ExecutionException,并且也没走我们的默认异常处理器。

并且当我们不 get() 获取结果的时候,什么报错也不会有,这是为什么?我们来看看:

我们首先看看 submit 方法:

public <T> Future<T> submit(Callable<T> task) {
    if (task == null) throw new NullPointerException();
    // 把我们的task 封装成了 FutureTask
    RunnableFuture<T> ftask = newTaskFor(task);
    // 执行
    execute(ftask);
    return ftask;
}

那我们再看看 FutureTask 的 run 方法:

public void run() {
...
    try {
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                result = c.call();
                ran = true;
            // 我们抛出的异常 会被这里吞掉
            } catch (Throwable ex) {
                result = null;
                ran = false;
                setException(ex);
            }
...
}

protected void setException(Throwable t) {
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        outcome = t;
        UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
        finishCompletion();
    }
}

继续再来看看Future.get方法的实现:

public V get() throws InterruptedException, ExecutionException {
    int s = state;
    // 如果任务没有结束,则等待结束
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
    // 如果执行结束,则报告执行结果
    return report(s);
}

@SuppressWarnings("unchecked")
private V report(int s) throws ExecutionException {
    Object x = outcome;
    // 如果执行正常,则返回结果
    if (s == NORMAL)
        return (V)x;
    // 如果任务被取消,调用get则报CancellationException
    if (s >= CANCELLED)
        throw new CancellationException();
    // 执行异常,则抛出ExecutionException
    throw new ExecutionException((Throwable)x);
}

在执行任务出现异常之后,异常存到了一个 outcome 字段中,只有在调用 get 方法获取 FutureTask 结果的时候,才会以 ExecutionException 的形式重新抛出异常。

如果一个由submit提交的任务由于抛出了异常而结束,那么这个异常将被Future.get封装在ExecutionException中重新抛出。所以,通过submit提交到线程池的任务,无论是抛出的未检查异常还是已检查异常,都将被认为是任务返回状态的一部分,因此不会交由异常处理器来处理。

那么我们上边的疑惑:

(1)当线程发生异常的时候,为什么不走默认的异常处理器? 因为 FutureTask 把我们抛出的异常捕获掉了,所以线程就不会异常,就走不到我们设置的异常处理器

(2)当调用 get() 获取的结果,自然就是根据线程实际执行的结果,如果有执行异常,会以ExecutionException 的形式重新抛出异常,跟我们的异常处理器搭不上关系了。

所以要记住 submit 和 直接 execute 的差异。

3  小结

好啦,本节关于线程的异常处理器就看到这里,有理解不对的地方欢迎指正哈。

posted @ 2024-06-18 08:53  酷酷-  阅读(139)  评论(0编辑  收藏  举报