java多线程, 该如何处理异常?

avatar
作者
猴君
阅读量:0

目录

如何处理线程运行时异常

 UncaughtExceptionHandler

没有注入 未捕捉异常处理器, 线程是如何处理异常的? 


看的时候, 希望自己能在idea中跟着ctrl + 鼠标左键, 点一遍. . .


如何处理线程运行时异常

先来了解一下java的异常: 

        在Java中,异常(Exception)是程序执行过程中出现的问题,这些问题会中断正常的程序流程。Java的异常处理机制允许我们以一种结构化和可预测的方式来处理这些问题。根据是否需要强制处理,Java中的异常被分为两大类:checked异常(受检异常)和unchecked异常(非受检异常)。

  • Checked异常(受检异常)

        Checked异常,也称为受检异常,是那些必须在方法签名中声明(使用throws关键字)或者在方法体内被捕获(使用try-catch语句)的异常。这种机制强制程序员处理可能会发生的错误情况,从而提高了程序的健壮性和可靠性。

        Checked异常通常是那些可以合理地从发生异常的方法中恢复的情况,或者至少是可以被调用者所预见的。例如,当你尝试打开一个不存在的文件时,会抛出FileNotFoundException,这是一个checked异常。调用者必须显式地处理这个异常,要么通过捕获它,要么通过在自己的方法签名中声明它,从而将责任传递给调用者。

  • Unchecked异常(非受检异常)

        相比之下,Unchecked异常(包括运行时异常RuntimeException和错误Error)不需要在方法签名中声明,也不需要在方法体内被捕获。这些异常通常是由编程错误或系统资源不足等不可预见的问题引起的,因此它们通常表示程序中的严重问题,这些问题在程序运行时可能无法恢复或处理。

Thread类关于线程运行时异常处理的 四个API :

  • public void setUncaughtExceptionHandler (UncaughtExceptionHandler eh):为某个特定线程指定 UncaughtExceptionHandler。
  • Ipublic static void setDefaultUncaughtExceptionHandler ( UncaughtExceptionHandlereh):设置全局的 UncaughtExceptionHandler。
  • public UncaughtExceptionHandler getUncaughtExceptionHandler():获取特定线程的UncaughtExceptionHandler.
  • public static UncaughtExceptionHandler getDefaultUncaughtExceptionHandler():获取全局的 UncaughtExceptionHandler.

 UncaughtExceptionHandler

        线程在自己的执行单元中是不允许抛出checked异常的(你可以发现无论是实现Runnable接口还是继承Thread类, 其run方法都没有声明抛出任何checked异常) :

@FunctionalInterface public interface Runnable {     /**      * Runs this operation.      */     void run(); }

        如果你尝试在run()方法中抛出一个checked异常,编译器会报错

         因此线程的执行单元是不允许抛出checked异常的, 同时我们知道, 非受检异常是不需要声明或者抛出的, 它会在抛出的调用栈里面, 逐层网上抛出, 最后直到被JVM自动捕捉, 或者是被主动处理.  因此你可以在一个线程中主动调用wait方法来阻塞当前线程(在Java中,wait() 方法是一个在对象监视器(monitor)上等待的线程阻塞机制,它属于 java.lang.Object 类的一部分。wait() 方法的设计和使用方式与其他非受检异常(如 NullPointerException)的处理有所不同,这主要是因为它抛出了一个受检异常 InterruptedException, 所以你应该显示的处理InterruptedException异常)

         但是, 线程会运行在自己的上下文, 派生它的线程, 是无法直接获取它运行中出现的异常信息, 因此java给我们提供了一个回调接口, 这个接口就为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 是一个接口, 里面有一个抽象方法接口, 该接口会被Thread类中的dispatchUncaughtException调用 

    // Thread类 中, This method is called when a thread terminates with an exception.     void dispatchUncaughtException(Throwable e) {         getUncaughtExceptionHandler().uncaughtException(this, e);     }
    public UncaughtExceptionHandler getUncaughtExceptionHandler() {         if (isTerminated()) {             // uncaughtExceptionHandler may be set to null after thread terminates             return null;         } else {             UncaughtExceptionHandler ueh = uncaughtExceptionHandler;             return (ueh != null) ? ueh : getThreadGroup();         }     }

当执行中的线程出现异常的时候, 这个方法就会被调用, 从而调用getUncaughtExceptionHandler方法获取一个UncaughtExceptionHandler 处理器实例, 然后调用这个实例的回调接口, 去对异常做出相关处理. 

首先我们来看看这个获取处理器实例的代码: 

        if (isTerminated()) {             // uncaughtExceptionHandler may be set to null after thread terminates             return null;         } else {             UncaughtExceptionHandler ueh = uncaughtExceptionHandler;             return (ueh != null) ? ueh : getThreadGroup();         }

我们做出如下分析: 

  • 方法首先调用isTerminated()来检查线程是否已经终止, 如果线程已经终止了, 那么你处理这个异常也就没有意义了, 也是就不需要返回处理器实例了. 线程已经终止, 它的未捕获异常处理器可能不再有效或需要被重置为null,以避免对无效资源的引用
  • 如果线程尚未终止,方法接着尝试获取当前设置的未捕获异常处理器, 这个处理器就是当前线程的一个属性, 你可以通过get和set方法来获取和设置
    private volatile UncaughtExceptionHandler uncaughtExceptionHandler;      // set     public void setUncaughtExceptionHandler(UncaughtExceptionHandler ueh) {         checkAccess();         uncaughtExceptionHandler(ueh);     }      // get     public UncaughtExceptionHandler getUncaughtExceptionHandler() {         if (isTerminated()) {             // uncaughtExceptionHandler may be set to null after thread terminates             return null;         } else {             UncaughtExceptionHandler ueh = uncaughtExceptionHandler;             return (ueh != null) ? ueh : getThreadGroup();         }     } 
  • 如果不为空就返回这个处理器实例
  • 如果为空就调用getThreadGroup方法, 如下: 
    public final ThreadGroup getThreadGroup() {         if (isTerminated()) {             return null;         } else {             return isVirtual() ? virtualThreadGroup() : holder.group;         }     }
  • 首先检查是否终止, 原因如上面所述
  • 接下来,方法通过调用 isVirtual() 来检查当前对象是否代表一个“虚拟线程”(Virtual Thread)。虚拟线程是Java 17中引入的一个新特性,旨在提供一种轻量级的线程实现,适用于高并发场景。这个检查表明该代码是为支持虚拟线程而设计的。
    public final boolean isVirtual() {         return (this instanceof BaseVirtualThread);     }

插一嘴: 什么是虚拟线程, 什么是常规线程

  • 常规线程是Java平台上用于实现并发执行的一种机制。它是程序执行流的最小单元,可以执行程序代码中的任务。在Java中,线程通常通过继承Thread类或者实现Runnable接口来创建
  • 虚拟线程是Java 19(预览版)引入的一种新特性,正式发布于JDK 21。它们是一种轻量级的执行上下文,不直接和操作系统的物理线程一一对应,而是在Java虚拟机(JVM)层面实现的逻辑线程。
  •  如果是虚拟线程, 那么就返回线程组
    static ThreadGroup virtualThreadGroup() {         return Constants.VTHREAD_GROUP;     }  // ....      private static class Constants {         // Thread group for virtual threads.         static final ThreadGroup VTHREAD_GROUP;     }
  • 否则返回常规线程组(可以返回线程组的原因是线程组其实是实现了Thread.UncaughtExceptionHandler接口的)
  • 然后调用其uncaughtException方法来处理异常

下面是UncaughtExceptionHandler的使用案例: 

public class UncaughtExceptionHandlerExample {          // 实现 UncaughtExceptionHandler 接口       static class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {           @Override           public void uncaughtException(Thread t, Throwable e) {               System.out.println("异常被捕获:" + e.getMessage());               // 在这里可以添加日志记录、发送异常通知等处理逻辑           }       }          public static void main(String[] args) {           // 创建一个线程           Thread thread = new Thread(() -> {               // 故意制造一个未捕获的异常               throw new RuntimeException("这是一个未捕获的异常");           });              // 为线程设置 UncaughtExceptionHandler           thread.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());              // 启动线程           thread.start();              // 主线程继续执行其他任务...       }   }

        Runnable接口是没有声明任何异常的, 所以你不能再执行单元中写入任何会抛出受检异常的代码, 但是你可以写了受检异常的代码然后将其主动catch即可, 但是你写的代码是有可能包含非受检异常的, 这个时候可以使用UncaughtExceptionHandler 来自动捕捉, 然后处理.

         我都能自己catch了, 为啥还需要UncaughtExceptionHandler ?

        首先, 如果您不想在run方法内部处理这些受检异常,那么您可以选择让它们被捕获并包装成一个运行时异常(通常是RuntimeException或它的子类),然后这个运行时异常将不会被run方法的签名所限制,并且可以被线程的上层机制(如UncaughtExceptionHandler)捕获

        然而,通常情况下,我们不会将受检异常简单地包装成运行时异常来绕过编译器的检查,因为这样做会丢失异常的类型信息,并且可能会使调用者难以处理这些异常。相反,我们会尽量在run方法内部处理或声明这些受检异常,或者将它们作为Callable任务的一部分来执行,以便可以捕获并处理它们

        如果你无异疏忽了一个非受检异常, 那么当前线程就会中断, 如果其他线程依赖当前线程, 那么就会产生不可预估的错误. 

        关于UncaughtExceptionHandler,它的作用主要是捕获并处理那些在线程执行过程中未被捕获的异常。这些异常可能是由于编程错误(如遗漏了try-catch块)或由于异常发生在无法直接捕获它的上下文中(如run方法内部且未声明抛出)。当这些异常发生时,如果没有UncaughtExceptionHandler来处理它们,它们通常会导致线程突然终止,并且异常信息可能会被忽略或仅通过标准错误输出(stderr)打印出来

没有注入 未捕捉异常处理器, 线程是如何处理异常的? 

首先一个线程在初始化的时候的几个方法: 

填充的参数里面就没有一个UncaughtExceptionHandler类型的参数, 也就是说他可能没有默认的未捕捉异常处理器, 我们接着看: 

    /**      * Dispatch an uncaught exception to the handler. This method is      * called when a thread terminates with an exception.      */     void dispatchUncaughtException(Throwable e) {         getUncaughtExceptionHandler().uncaughtException(this, e);     }

发生异常之后, 就会调用这个方法, 首先你得获取一个UncaughtExceptionHandler处理器的实例, 然后调用他的异常处理方法. 最复杂的逻辑其实就是在这个后去UncaughtExceptionHandler的身上.

    public UncaughtExceptionHandler getUncaughtExceptionHandler() {         if (isTerminated()) {             // uncaughtExceptionHandler may be set to null after thread terminates             return null;         } else {             UncaughtExceptionHandler ueh = uncaughtExceptionHandler;             return (ueh != null) ? ueh : getThreadGroup();         }     }
    // Null,除非显式设置     private volatile UncaughtExceptionHandler uncaughtExceptionHandler;

上面我们已经讲解过, 这里不再赘述, 可以看出来, 我们如果直接去获取肯定是null的, 如实就获取了当前线程的线程组的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 {                 System.err.print("Exception in thread \"" + t.getName() + "\" ");                 e.printStackTrace(System.err);             }         }     }

解析一下这个代码: 

    private final ThreadGroup parent;

首先这个parent是当前线程组的父线程组.

        if (parent != null) {             parent.uncaughtException(t, e);

如果不为空, 就调用父类的异常处理方法(事实上, 如果父类没有显示的重写, 那么也将调用上述的代码), 如果父类一直调用parent的处理方法, 那么就会一直到最顶层处理器, 直到遇到了被重写的处理方法, 并且这个处理方法没有继续调用上一级的处理器的处理方法.

如果父级为空. 那么就调用Thread类的静态方法getDefaultUncaughtExceptionHandler, 来获取默认的处理器, 我们来看看这个方法: 

    // Thread类中的静态方法     public static UncaughtExceptionHandler getDefaultUncaughtExceptionHandler(){         return defaultUncaughtExceptionHandler;     }          // Thread类中的属性,Null,除非显式设置     private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler; 

可以看到, 其实这个defaultUncaughtExceptionHandler, 可以理解为一个全局的处理机制, 也就是说: 

        如果你的Thread实例显示的设置了 UncaughtExceptionHandler处理器, 那么你就会在你的线程的执行任务中 使用你当前线程设置的异常处理器, 否则就会调用你当前线程组的处理器的处理方法, 父线程组的处理方法是一只调用其父线程组的异常处理方法. 直到没有父线程组之后, 再去调用Thread的默认处理器

        除非你重写当前处理器的逻辑, 让线程组的默认处理方法不再调用上级的线程组的处理方法. 而是直接使用你重写的逻辑进行处理

        如果上述的这些处理器(线程异常处理器, Thread默认处理器) 为空, 并且你的线程组的处理逻辑也是一直寻找父线程组的处理器. 那么就会抛出异常: 

System.err.print("Exception in thread \"" + t.getName() + "\" "); e.printStackTrace(System.err);  // System.err     public static final PrintStream err = null; 

此处的t是当前线程

总结就是

线程handler 为null 就找 线程组的 handler, 如果线程组的handler没有直接处理, 而是网上找, 那么最后就会找到Thread 类默认的handler

 

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!