展开菜单
首页 精品内容 本月促销 装机必备 Windows PDF教程 macOS软件 IOS软件 Android AI 专题
全部分类

当前位置:

首页 > 编程开发 > Java线程的异常处理机制是什么

Java线程的异常处理机制是什么

前言启动一个Java程序,本质上是运行某个Java类的main方法。我们写一个死循环程序,跑起来,然后运行jvisualvm进行观察可以看到这个Java进程中,一共有11个线程,其中10个守护线程,1个用户线程。我们main方法中的代码,就跑在一个名为main的线程中。当Java进程中跑着的所有线程都是守护线程时,JVM就会退出。在单线程的场景下,如果代码运行到某个位置时抛出了异常,会看到控制台打印出异常的堆栈信息。但在多线程的场景下,子线程中发生的异常,不一定就能及时的将异常信息打印出来。我曾经在工作中

本文目录

      前言

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

      Java线程的异常处理机制是什么

      可以看到这个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.
               * 

      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方法,看一下线程的情况

      Java线程的异常处理机制是什么

      Java线程的异常处理机制是什么

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

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

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

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

      Java线程的异常处理机制是什么

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

      小结

      所以,正常来说,如果没有对某个线程设置特定的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方法中,会执行到下图红框的部分

      Java线程的异常处理机制是什么

      Java线程的异常处理机制是什么

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

      多扯几句别的。

      上面跟源码时,注意到Worker是ThreadPoolExecutor的一个内部类,也就是说,每个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方法

      Java线程的异常处理机制是什么

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

      它们之间有一种嵌套依赖的关系。每个Worker里持有一个Thread对象,这个Thread对象又是以这个Worker对象作为Runnable,而Worker又是ThreadPoolExecutor的内部类,这意味着每个Worker对象都会隐式的持有其所属的ThreadPoolExecutor对象的引用。每个Worker的run方法, 都跑在子线程中,但是这些Worker跑在子线程中时,能够对ThreadPoolExecutor对象的属性进行访问和修改(每个Worker的run方法都是调用的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 ftask = newTaskFor(task, null);
              execute(ftask);
              return ftask;
          }
          protected  RunnableFuture newTaskFor(Runnable runnable, T value) {
              return new FutureTask(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  Callable callable(Runnable task, T result) {
              if (task == null)
                  throw new NullPointerException();
              return new RunnableAdapter(task, result);
          }
          static final class RunnableAdapter implements Callable {
              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,又实现了Runnable和Future接口

      Java线程的异常处理机制是什么

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

      Java线程的异常处理机制是什么

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

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

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

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

          public Future submit(Runnable task) {
              if (task == null) throw new NullPointerException();
              RunnableFuture 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); // 异常会通过这一句被抛出来
          }

      小结

      • 通过ThreadPoolExecutor的execute方法提交的任务,出现异常后,异常会在子线程中被抛出,并被JVM捕获,并调用子线程的dispatchUncaughtException方法,进行异常处理,若子线程没有任何特殊设置,则异常堆栈会被输出到System.err,即异常会被打印到控制台上。并且会从线程池中移除当前Worker,并另启一个新的Worker作为替代。

      • 通过ThreadPoolExecutor的submit方法提交的任务,任务会先被包装成FutureTask对象,出现异常后,异常会被生吞,并暂存到FutureTask对象中,作为任务执行结果的一部分。异常信息不会被打印,该子线程也不会被线程池移除(因为异常在子线程中被吞了,没有抛出来)。在调用FutureTask上的get方法时(此时一般是在主线程中了),异常才会被抛出,触发主线程的异常处理,并输出到System.err

      其他

      其他的线程池场景

      比如:

      • 使用ScheduledThreadPoolExecutor实现延迟任务或者定时任务(周期任务),分析过程也是类似。这里给个简单结论,当调用scheduleAtFixedRate方法执行一个周期任务时(任务会被包装成FutureTask (实际是ScheduledFutureTask ,是FutureTask 的子类)),若周期任务中出现异常,异常会被生吞,异常信息不会被打印,线程不会被回收,但是周期任务执行这一次后就不会继续执行了。ScheduledThreadPoolExecutor继承了ThreadPoolExecutor,所以其也是复用了ThreadPoolExecutor的那一套逻辑。

      • 使用CompletableFuture 的runAsync 提交任务,底层是通过ForkJoinPool 线程池进行执行,任务会被包装成AsyncRun ,且会返回一个CompletableFuture 给主线程。当任务出现异常时,处理方式和ThreadPoolExecutor 的submit 类似,异常堆栈不会被打印。只有在CompletableFuture 上调用get 方法尝试获取结果时,异常才会被打印。

      本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
      作者最新文章
      编程开发 Java
      相关文章 更多
      精品专题 更多
      装机必备

      正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

      Windows

      正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

      PDF教程

      正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

      macOS软件

      正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

      IOS软件

      正软商城iOS软件专区,精选适用于iPhone和iPad的办公、学习、影音、设计、效率及AI应用,提供功能介绍、适用设备、系统要求和正版获取方式等信息。

      AI

      正软商城AI软件专区,汇集AI写作、AI绘画、AI视频、AI办公、AI编程、AI翻译、智能客服和数据分析等人工智能工具,提供功能介绍、适用平台、收费方式及正版购买信息。

      Mac软件 更多
      讯飞智作 网页版
      讯飞智作 网页版

      讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

      火山引擎 网页版
      火山引擎 网页版

      火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

      Aperty macOS版
      Aperty macOS版

      Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

      立刻MV 网页版
      立刻MV 网页版

      立刻MV 面向音乐人、内容创作者和品牌团队,帮助用户低成本完成歌曲 MV、歌词视频、AI 分镜、图生视频与最终合成。

      蛙蛙写作 网页版
      蛙蛙写作 网页版

      蛙蛙写作 AI 创作工具,覆盖小说、剧本、文案,一键生成文稿,打通文字转视频全流程,高效解决创作难题。

      超级简历WonderCV 网页版
      超级简历WonderCV 网页版

      超级简历WonderCV是面向应届生和AI小白的智能求职平台,覆盖简历、面试与职业规划。

      音述AI 网页版
      音述AI 网页版

      音述AI,是一个以“用声音述说故事”为核心的 AI 音乐创作与声音分享社区。

      AionClaw macOS版
      AionClaw macOS版

      AionClaw 是可在桌面端运行的任务型 AI 助手,通过数字专家与技能市场组合办公、创作、数据和开发能力。

      Loomy macOS版
      Loomy macOS版

      Loomy 是科大讯飞推出的桌面 AI 助手,可通过对话、任务与工作目录协助处理办公、内容创作和资料整理。

      AirBuddy 3 macOS版
      AirBuddy 3 macOS版

      AirBuddy 3 是面向 macOS 的蓝牙外设增强工具,可集中查看 AirPods、Beats 与苹果设备电量,提供快速连接、播放控制、电量提醒、桌面小组件和多台 Mac 外设接力等功能。

      AirServer macOS版
      AirServer macOS版

      AirServer macOS版可把 Mac 转换为无线投屏接收端,通过 AirPlay 或 Google Cast 接收手机、平板和电脑画面,并支持多设备同时连接、高清镜像及会话录制,适合家庭娱乐、课堂教学、会议演示和应用展示。

      AnyDesk macOS版
      AnyDesk macOS版

      AnyDesk macOS版是一款适用于远程办公、设备维护和客户支持的远程桌面工具,可连接并操作异地计算机,支持文件传输、无人值守访问、会话权限控制及跨平台协作。

      WINDOWS 更多
      讯飞智作 网页版
      讯飞智作 网页版

      讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

      火山引擎 网页版
      火山引擎 网页版

      火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

      PixTV 网页版
      PixTV 网页版

      PixTV 是一款在浏览器中使用的 AI 视觉内容创作平台,提供图片、视频、音频、文字生成、AI 分镜、局部重绘、提示词反推和无限画布等工具,适合短剧、漫剧、广告及社交媒体内容制作。

      讯飞绘文 网页版
      讯飞绘文 网页版

      讯飞绘文网页版是科大讯飞推出的AI图文创作与内容运营工具,覆盖热点选题、文章生成、稿件校对、智能配图、排版发布和数据复盘等环节,适合自媒体创作者及内容团队在线使用。

      Maya(Autodesk Maya) Windows版
      Maya(Autodesk Maya) Windows版

      Autodesk Maya 是面向影视、电视、动画和游戏行业的专业三维制作软件,集多边形与 NURBS 建模、角色绑定、关键帧和动作捕捉动画、Bifrost 特效模拟、材质开发及 Arnold 渲染于一体,

      Aperty Windows版
      Aperty Windows版

      Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

      立刻MV 网页版
      立刻MV 网页版

      立刻MV 面向音乐人、内容创作者和品牌团队,帮助用户低成本完成歌曲 MV、歌词视频、AI 分镜、图生视频与最终合成。

      蛙蛙写作 网页版
      蛙蛙写作 网页版

      蛙蛙写作 AI 创作工具,覆盖小说、剧本、文案,一键生成文稿,打通文字转视频全流程,高效解决创作难题。

      超级简历WonderCV 网页版
      超级简历WonderCV 网页版

      超级简历WonderCV是面向应届生和AI小白的智能求职平台,覆盖简历、面试与职业规划。

      音述AI 网页版
      音述AI 网页版

      音述AI,是一个以“用声音述说故事”为核心的 AI 音乐创作与声音分享社区。

      AionClaw Windows版
      AionClaw Windows版

      AionClaw 是可在桌面端运行的任务型 AI 助手,通过数字专家与技能市场组合办公、创作、数据和开发能力。

      Loomy Windows版
      Loomy Windows版

      Loomy 是科大讯飞推出的桌面 AI 助手,可通过对话、任务与工作目录协助处理办公、内容创作和资料整理。