Java线程的异常处理机制详情

目录
  • 前言
  • 线程的异常处理机制
    • 小结
  • 线程池场景下的异常处理
    • 小结
  • 其他

前言

启动一个Java程序,本质上是运行某个Java类的main方法。我们写一个死循环程序,跑起来,然后运行jvisualvm进行观察

可以看到这个Java进程中,一共有11个线程,其中10个守护线程,1个用户线程。我们main方法中的代码,就跑在一个名为main的线程中。当Java进程中跑着的所有线程都是守护线程时,JVM就会退出

在单线程的场景下,如果代码运行到某个位置时抛出了异常,会看到控制台打印出异常的堆栈信息。但在多线程的场景下,子线程中发生的异常,不一定就能及时的将异常信息打印出来。

我曾经在工作中遇到过一次,采用CompletableFuture.runAsync异步处理耗时任务时,任务处理过程中出现异常,然而日志中没有任何关于异常的信息。时隔许久,重新温习了线程中的异常处理机制,加深了对线程工作原理的理解,特此记录。

线程的异常处理机制

我们知道,Java程序的运行,是先经由javac将Java源代码编译成class字节码文件,然后由JVM加载并解析class文件,随后从主类的main方法开始执行。当一个线程在运行过程中抛出了未捕获异常时,会由JVM调用这个线程对象上的dispatchUncaughtException方法,进行异常处理。

// Thread类中
private void dispatchUncaughtException(Throwable e) {
        getUncaughtExceptionHandler().uncaughtException(this, e);
}

源码很好理解,先获取一个UncaughtExceptionHandler异常处理器,然后通过调用这个异常处理器的uncaughtException方法来对异常进行处理。(下文用缩写ueh来表示UncaughtExceptionHandler

ueh是个 啥呢?其实就是定义在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);
    }

再来看下Thread对象中的getUncaughtExceptionHandler方法

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

先查看当前这个Thread对象是否有设置自定义的ueh对象,若有,则由其对异常进行处理,否则,由当前Thread对象所属的线程组(ThreadGroup)进行异常处理。我们点开源码,容易发现ThreadGroup类本身实现了Thread.UncaughtExceptionHandler接口,也就是说ThreadGroup本身就是个异常处理器。

public class ThreadGroup implements Thread.UncaughtExceptionHandler {
    private final ThreadGroup parent;
    ....
}

假设我们在main方法中抛出一个异常,若没有对main线程设置自定义的ueh对象,则交由main线程所属的ThreadGroup来处理异常。我们看下ThreadGroup是怎么处理异常的:

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

这部分源码也比较简短。首先是查看当前ThreadGroup是否拥有父级的ThreadGroup,若有,则调用父级ThreadGroup进行异常处理。否则,调用静态方法Thread.getDefaultUncaughtExceptionHandler()获取一个默认ueh对象。

默认ueh对象不为空,则由这个默认的ueh对象进行异常处理;否则,当异常不是ThreadDeath时,直接将当前线程的名字,和异常的堆栈信息,通过标准错误输出System.err)打印到控制台。

我们随便运行一个main方法,看一下线程的情况

可以看到,main线程属于一个同样名为mainThreadGroup,而这个mainThreadGroup,其父级ThreadGroup名为system,而这个systemThreadGroup,没有父级了,它就是根ThreadGroup

由此可知,main线程中抛出的未捕获异常,最终会交由名为systemThreadGroup进行异常处理,而由于没有设置默认ueh对象,异常信息会通过System.err输出到控制台。

接下来,我们通过最朴素的方式(new一个Thread),在main线程中创建一个子线程,在子线程中编写能抛出异常的代码,进行观察

    public static void main(String[] args)  {
        Thread thread = new Thread(() -> {
            System.out.println(3 / 0);
        });
        thread.start();
    }

子线程中的异常信息被打印到了控制台。异常处理的流程就是我们上面描述的那样。

小结

所以,正常来说,如果没有对某个线程设置特定的ueh对象;也没有调用静态方法Thread.setDefaultUncaughtExceptionHandler设置全局默认ueh对象。那么,在任意一个线程的运行过程中抛出未捕获异常时,异常信息都会被输出到控制台(当异常是ThreadDeath时则不会进行输出,但通常来说,异常都不是ThreadDeath,不过这个细节要注意下)。

如何设置自定义的ueh对象来进行异常处理?根据上面的分析可知,有2种方式

  • 对某一个Thread对象,调用其setUncaughtExceptionHandler方法,设置一个ueh对象。注意这个ueh对象只对这个线程起作用
  • 调用静态方法Thread.setDefaultUncaughtExceptionHandler()设置一个全局默认ueh对象。这样设置的ueh对象会对所有线程起作用

当然,由于ThreadGroup本身可以充当ueh,所以其实还可以实现一个ThreadGroup子类,重写其uncaughtException方法进行异常处理。

若一个线程没有进行任何设置,当在这个线程内抛出异常后,默认会将线程名称和异常堆栈,通过System.err进行输出。

线程的异常处理机制,用一个流程图表示如下:

线程池场景下的异常处理

在实际的开发中,我们经常会使用线程池来进行多线程的管理和控制,而不是通过new来手动创建Thread对象。

对于Java中的线程池ThreadPoolExecutor,我们知道,通常来说有两种方式,可以向线程池提交任务:

  • execute
  • submit

其中execute方法没有返回值,我们通过execute提交的任务,只需要提交该任务给线程池执行,而不需要获取任务的执行结果。而submit方法,会返回一个Future对象,我们通过submit提交的任务,可以通过这个Future对象,拿到任务的执行结果。

我们分别尝试如下代码:

    public static void main(String[] args)  {
        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        threadPool.execute(() -> {
            System.out.println(3 / 0);
        });
    }
    public static void main(String[] args)  {
        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        threadPool.submit(() -> {
            System.out.println(3 / 0);
        });
    }

容易得到如下结果:

通过execute方法提交的任务,异常信息被打印到控制台;通过submit方法提交的任务,没有出现异常信息。

我们稍微跟一下ThreadPoolExecutor的源码,当使用execute方法提交任务时,在runWorker方法中,会执行到下图红框的部分

在上面的代码执行完毕后,由于异常被throw了出来,所以会由JVM捕捉到,并调用当前子线程dispatchUncaughtException方法进行处理,根据上面的分析,最终异常堆栈会被打印到控制台。

多扯几句别的。

上面跟源码时,注意到WorkerThreadPoolExecutor的一个内部类,也就是说,每个Worker都会隐式的持有ThreadPoolExecutor对象的引用(内部类的相关原理请自行补课)。每个Worker在运行时(在不同的子线程中运行)都能够对ThreadPoolExecutor对象(通常来说这个对象是在main线程中被维护)中的属性进行访问和修改。Worker实现了Runnable接口,并且其run方法实际是调用的ThreadPoolExecutor上的runWorker方法。在新建一个Worker时,会创建一个新的Thread对象,并把当前Worker的引用传递给这个Thread对象,随后调用这个Thread对象的start方法,则开始在这个Thread中(子线程中)运行这个Worker

        Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }

ThreadPoolExecutor中的addWorker方法

再次跟源码时,加深了对ThreadPoolExecutorWorker体系的理解和认识。

它们之间有一种嵌套依赖的关系。每个Worker里持有一个Thread对象,这个Thread对象又是以这个Worker对象作为Runnable,而Worker又是ThreadPoolExecutor的内部类,这意味着每个Worker对象都会隐式的持有其所属的ThreadPoolExecutor对象的引用。每个Workerrun方法, 都跑在子线程中,但是这些Worker跑在子线程中时,能够对ThreadPoolExecutor对象的属性进行访问和修改(每个Workerrun方法都是调用的runWorker,所以runWorker方法是跑在子线程中的,这个方法中会对线程池的状态进行访问和修改,比如当前子线程运行过程中抛出异常时,会从ThreadPoolExecutor中移除当前Worker,并启一个新的Worker)。而通常来说,ThreadPoolExecutor对象的引用,我们通常是在主线程中进行维护的。

反正就是这中间其实有点骚东西,没那么简单。需要多跟几次源码,多自己打断点进行debug,debug过程中可以通过IDEA的Evaluate Expression功能实时观察当前方法执行时所处的线程环境(Thread.currentThread)。

扯得有点远了,现在回到正题。上面说了调用ThreadPoolExecutor中的execute方法提交任务,子线程中出现异常时,异常会被抛出,打印在控制台,并且当前Worker会被线程池回收,并重启一个新的Worker作为替代。那么,调用submit时,异常为何就没有被打印到控制台呢?

我们看一下源码:

    public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }
    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
    }

通过调用submit提交的任务,被包装了成了一个FutureTask对象,随后会将这个FutureTask对象,通过execute方法提交给线程池,并返回FutureTask对象给主线程的调用者。

也就是说,submit方法实际做了这几件事

  • 将提交的Runnable,包装成FutureTask
  • 调用execute方法提交这个FutureTask(实际还是通过execute提交的任务)
  • FutureTask作为返回值,返回给主线程的调用者

关键就在于FutureTask,我们来看一下

    public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // ensure visibility of callable
    }
    // Executors中
	public static <T> Callable<T> callable(Runnable task, T result) {
        if (task == null)
            throw new NullPointerException();
        return new RunnableAdapter<T>(task, result);
    }
    static final class RunnableAdapter<T> implements Callable<T> {
        final Runnable task;
        final T result;
        RunnableAdapter(Runnable task, T result) {
            this.task = task;
            this.result = result;
        }
        public T call() {
            task.run();
            return result;
        }
    }

通过submit方法传入的Runnable,通过一个适配器RunnableAdapter转化为了Callable对象,并最终包装成为一个FutureTask对象。这个FutureTask,又实现了RunnableFuture接口

于是我们看下FutureTaskrun方法(因为最终是将包装后的FutureTask提交给线程池执行,所以最终会执行FutureTaskrun方法)

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

可以看到,异常信息只是被简单的设置到了FutureTaskoutcome字段上。并没有往外抛,所以这里其实相当于把异常给生吞了catch块中捕捉到异常后,既没有打印异常的堆栈,也没有把异常继续往外throw。所以我们无法在控制台看到异常信息,在实际的项目中,此种场景下的异常信息也不会被输出到日志文件。这一点要特别注意,会加大问题的排查难度。

那么,为什么要这样处理呢?

因为我们通过submit提交任务时,会拿到一个Future对象

    public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }

我们可以在稍后,通过Future对象,来获知任务的执行情况,包括任务是否成功执行完毕,任务执行后返回的结果是什么,执行过程中是否出现异常。

所以,通过submit提交的任务,实际会把任务的各种状态信息,都封装在FutureTask对象中。当最后调用FutureTask对象上的get方法,尝试获取任务执行结果时,才能够看到异常信息被打印出来。

    public V get() throws InterruptedException, ExecutionException {
        int s = state;
        if (s <= COMPLETING)
            s = awaitDone(false, 0L);
        return report(s);
    }
    private V report(int s) throws ExecutionException {
        Object x = outcome;
        if (s == NORMAL)
            return (V)x;
        if (s >= CANCELLED)
            throw new CancellationException();
        throw new ExecutionException((Throwable)x); // 异常会通过这一句被抛出来
    }

小结

  • 通过ThreadPoolExecutorexecute方法提交的任务,出现异常后,异常会在子线程中被抛出,并被JVM捕获,并调用子线程的dispatchUncaughtException方法,进行异常处理,若子线程没有任何特殊设置,则异常堆栈会被输出到System.err,即异常会被打印到控制台上。并且会从线程池中移除当前Worker,并另启一个新的Worker作为替代。
  • 通过ThreadPoolExecutorsubmit方法提交的任务,任务会先被包装成FutureTask对象,出现异常后,异常会被生吞,并暂存到FutureTask对象中,作为任务执行结果的一部分。异常信息不会被打印该子线程也不会被线程池移除(因为异常在子线程中被吞了,没有抛出来)。在调用FutureTask上的get方法时(此时一般是在主线程中了),异常才会被抛出,触发主线程的异常处理,并输出到System.err

其他

其他的线程池场景

比如:

  • 使用ScheduledThreadPoolExecutor实现延迟任务或者定时任务(周期任务),分析过程也是类似。这里给个简单结论,当调用scheduleAtFixedRate方法执行一个周期任务时(任务会被包装成FutureTask (实际是ScheduledFutureTask ,是FutureTask 的子类)),若周期任务中出现异常,异常会被生吞,异常信息不会被打印,线程不会被回收,但是周期任务执行这一次后就不会继续执行了。ScheduledThreadPoolExecutor继承了ThreadPoolExecutor,所以其也是复用了ThreadPoolExecutor的那一套逻辑。
  • 使用CompletableFuture runAsync 提交任务,底层是通过ForkJoinPool 线程池进行执行,任务会被包装成AsyncRun ,且会返回一个CompletableFuture 给主线程。当任务出现异常时,处理方式和ThreadPoolExecutor 的submit 类似,异常堆栈不会被打印。只有在CompletableFuture 上调用get 方法尝试获取结果时,异常才会被打印。

到此这篇关于Java线程的异常处理机制详情的文章就介绍到这了,更多相关Java线程异常处理内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java线程组与未处理异常实例分析

    本文实例讲述了Java线程组与未处理异常.分享给大家供大家参考,具体如下: 一 点睛 从JDK1.5开始,Java加强了线程的异常处理,如果线程执行过程中抛出了一个未处理的异常,JVM在结束该线程之前会自动查找是否有对应的Thread.UncaughtExceptionHandler对象,如果找到该处理器对象,将会调用该对象的uncaughtException(Thread t, Throwable e)方法来处理该异常. Thread.UncaughtExceptionHandler是Thre

  • Java并发编程示例(八):处理线程的非受检异常

    Java语言中,把异常分为两类: 受检异常: 这类异常必须在throws子句中被显式抛出或者在方法内被捕获.例如,IOException异常或ClassNotFoundException异常. 非受检异常: 这类异常不需要显式抛出或捕获.例如,NumberFormatException异常. 当一个受检异常在Thread对象的run()方法中被抛出时,我们必须捕获并处理它,因为run()方法不能抛出异常.而一个非受检异常在Thread对象的run()方法中被抛出时,默认的行为是在控制台打印出堆栈

  • java多线程中的异常处理机制简析

    在java多线程程序中,所有线程都不允许抛出未捕获的checked exception,也就是说各个线程需要自己把自己的checked exception处理掉.这一点是通过java.lang.Runnable.run()方法声明(因为此方法声明上没有throw exception部分)进行了约束.但是线程依然有可能抛出unchecked exception,当此类异常跑抛出时,线程就会终结,而对于主线程和其他线程完全不受影响,且完全感知不到某个线程抛出的异常(也是说完全无法catch到这个异常

  • 基于Java子线程中的异常处理方法(通用)

    在普通的单线程程序中,捕获异常只需要通过try ... catch ... finally ...代码块就可以了.那么,在并发情况下,比如在父线程中启动了子线程,如何在父线程中捕获来自子线程的异常,从而进行相应的处理呢? 常见错误 也许有人会觉得,很简单嘛,直接在父线程启动子线程的地方try ... catch一把就可以了,其实这是不对的. 原因分析 让我们回忆一下Runnable接口的run方法的完整签名,因为没有标识throws语句,所以方法是不会抛出checked异常的.至于Runtime

  • Java线程的异常处理机制详情

    目录 前言 线程的异常处理机制 小结 线程池场景下的异常处理 小结 其他 前言 启动一个Java程序,本质上是运行某个Java类的main方法.我们写一个死循环程序,跑起来,然后运行jvisualvm进行观察 可以看到这个Java进程中,一共有11个线程,其中10个守护线程,1个用户线程.我们main方法中的代码,就跑在一个名为main的线程中.当Java进程中跑着的所有线程都是守护线程时,JVM就会退出. 在单线程的场景下,如果代码运行到某个位置时抛出了异常,会看到控制台打印出异常的堆栈信息.

  • 全面理解java中的异常处理机制

    一.java异常总结: 异常就是程序运行时出现不正常运行情况 1.异常由来: 通过java的类的形式对现实事物中问题的描述,并封住成了对象 其实就是java对不正常情况描述后的对象体现 2.对于问题的划分有两种:一种是严重的问题,一种是非严重的问题 对于严重的,java通过Error类来描述 对于Error一般不编写针对性的代码对其进行处理 对于非严重的,java通过Exception类来描述 对于Exception可以使用针对性的处理方式进行处理 3.常见的异常有:数组角标越界异常,空指针异常

  • 浅析Java中的异常处理机制

    异常处理机制 1.抛出异常 2.捕获异常 3.异常处理五个关键字: try.catch.finally.throw.throws 注意:假设要捕获多个异常:需要按照层级关系(异常体系结构) 从小到大! package exception; /** * Java 捕获和抛出异常: * 异常处理机制 * 1.抛出异常 * 2.捕获异常 * 3.异常处理五个关键字 * try.catch.finally.throw.throws * 注意:假设要捕获多个异常:需要按照层级关系(异常体系结构) 从小到大

  • JAVA异常处理机制之throws/throw使用情况的区别

    JAVA中throw和throws的区别:https://www.cnblogs.com/xiohao/p/3547443.html 区别:(摘自上面的博客) 1.throws出现在方法函数头:而throw出现在函数体. 2.throws表示出现异常的一种可能性,并不一定会发生这些异常:throw则是抛出了异常,执行throw则一定抛出了某种异常. 3.两者都是消极处理异常的方式(这里的消极并不是说这种方式不好),只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用

  • JAVA异常处理机制之throws/throw使用情况

    JAVA中throw和throws的区别:https://www.jb51.net/article/191065.htm 区别:(摘自上面的博客) 1.throws出现在方法函数头:而throw出现在函数体. 2.throws表示出现异常的一种可能性,并不一定会发生这些异常:throw则是抛出了异常,执行throw则一定抛出了某种异常. 3.两者都是消极处理异常的方式(这里的消极并不是说这种方式不好),只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理. 也就

  • Java超详细梳理异常处理机制

    目录 一.异常概述与异常体系结构 1. 异常概述 引入 概念 2. 分类 2.1 Error vs Exception 2.2 编译时异常vs运行时异常 3. 常见异常 3.1 分类 3.2 代码演示 二.异常处理机制 1. 概述 2. 异常处理机制一之try-catch-finally 2.1 语法格式 2.2 使用 2.3 代码演示 3. 异常处理机制二之声明抛出异常throws 3.1 语法格式 3.2 使用 4. try-catch-finally与throws的区别 5. 手动抛出异常

  • 详解Java的Exception异常机制

    一.前言 在Java中,我们在执行代码的过程中难免会遇到错误与Exception异常,可是我们一直都是锤头Coding而忽略了学习Exception这个东西!我们只是知道在发生Exception的地方让代码自动生成throw exception或者是使用try-catch括起来处理,那你了解Java的Exception吗?今天就让我们把一起来看看Java的Exception吧! 在Java中,我们的代码再出现错误的时候无非是两种情况:一是Error,一是异常Exception.如果是Error,

  • java异常(Exception)处理机制详解

    一. 异常的定义 在<Java编程思想>中这样定义 异常:阻止当前方法或作用域继续执行的问题.虽然java中有异常处理机制,但是要明确一点,决不应该用"正常"的态度来看待异常.绝对一点说异常就是某种意义上的错误,就是问题,它可能会导致程序失败.之所以java要提出异常处理机制,就是要告诉开发人员,你的程序出现了不正常的情况,请注意. 记得当初学习java的时候,异常总是搞不太清楚,不知道这个异常是什么意思,为什么会有这个机制?但是随着知识的积累逐渐也对异常有一点感觉了.举一

  • 带你了解Java中的异常处理(下)

    今天继续讲解java中的异常处理机制,主要介绍Exception家族的主要成员,自定义异常,以及异常处理的正确姿势. Exception家族 一图胜千言,先来看一张图. Exception这是一个父类,它有两个儿子,IOException和RuntimeException,每个儿子都很能生,所以它有着一堆的孙子,但其实,Exception家族还有一个大家伙,那就是Throwable,这是一个接口,看名字就知道意思,就是"可被抛出"嘛,它还有一个同父异母的哥哥,那就是Error,这家伙可

  • 带你了解Java中的异常处理(上)

    当当当当当当,各位看官,好久不见,甚是想念. 今天我们来聊聊Java里的一个小妖精,那就是异常. 什么是异常?什么是异常处理? 异常嘛,顾名思义就是不正常,(逃),是Java程序运行时,发生的预料之外的事情,它阻止了程序按照程序员的预期正常执行. 异常处理,应该说异常处理机制,就是专门用来制服这个小妖精的法宝.Java中的异常处理机制能让程序在异常发生时,按照代码的预先设定的异常处理逻辑,针对性地处理异常,让程序尽最大可能恢复正常并继续执行,且保持代码的清晰. 简而言之,Java异常处理就是能让

随机推荐