【线程基础】【七】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 小结
好啦,本节关于线程的异常处理器就看到这里,有理解不对的地方欢迎指正哈。