多线程执行任务抛出异常不打印日志

原因

实际原因出在我写的业务方法,在最外层没有包裹一个 try/catch 语句块,导致线程执行业务方法时内部抛出了未捕获的异常,但是线程本身没有写一个默认的异常处理方法,最终 JVM 的默认处理方式是将这个异常以及堆栈信息只会被打印到控制台,而不是写到日志中。

原理分析

  • 当一个线程因未捕获的异常而即将终止时,JVM 将使用 Thread.getUncaughtExceptionHandler() 查询该线程以获得其 UncaughtExceptionHandler
  • 调用该 handler 的 uncaughtException() 方法,将线程和异常作为参数传递。
    • 如果线程没有实现 uncaughtException() 方法,则搜索该线程的 ThreadGroup 的异常处理器。
    • ThreadGroup 中的默认异常处理器实现是将处理工作逐层委托给上层的 ThreadGroup,直到某个 ThreadGroup 的异常处理器能够处理该异常,否则一直传递到顶层的ThreadGroup。
    • 顶层 ThreadGroup 的异常处理器委托给默认的系统处理器(如果默认的处理器存在,默认情况下为空),否则把栈信息输出到 System.err,即输出到控制台

execute() 和 submit() 遇到未捕获异常

  • 如果使用 execute() 方法执行任务,在线程内部,对于未捕获的异常,并不会像在主线程中一样打印错误日志。线程 UncaughtExceptionHandler 接口中有一个 uncaughtException() 方法,如果没有实现该处理方法,JVM 最后的默认策略是使用 System.err.print("Exception in thread \"" + t.getName() + "\" ") 输出日志到控制台,但是不会写到项目的日志文件中。如果在线程外加上 try/catch 语句也不能捕获到线程内部的异常,因为实际上线程内部的异常最后已经被 JVM 处理了,实际有效的方法是在线程所执行的业务方法外部加一层 try/catch,或者实现 uncaughtException() 方法打印日志,来保证业务方法中抛出的异常最终能被我们捕获到并在日志中被看见。
  • 如果使用 submit() 方法来执行任务,内部抛出异常不做任何处理,异常既不会打印到日志中,也不会输出到控制台,看起来就好像异常被线程吞了。实际上异常信息被存储到了线程的结果信息中,通过 Future#get 方法就能获取到内部抛出的异常,然后对异常进行捕获和处理。

总结

  • 出现异常的线程会被线程池移除,线程池会新建一个线程放入
  • 在线程因未捕获的异常而面临死亡时会调用 Thread.UncaughtExceptionHandler.uncaughtException() 方法,默认会将异常信息打印到控制台
  • execute() 内部出现未捕获异常时,默认只会输出错误信息到控制台,不会出现在日志中
  • submit() 方法内部出现未捕获异常时,默认控制台和日志都不会有相关信息,需要通过 Future#get 方法来获取异常信息
  • 使用 exectue() 方法执行任务时,推荐挑选其中一种处理方式
    • 业务代码最外层加上 try/catch 语句块,输出错误日志,便于问题排查
    • 实现 UncaughtExceptionHandler 接口并为线程重写一个默认的 uncaughtException 方法,在 uncaughtException 方法中打印日志

参考

【Java技术指南】「技术盲区」看看线程以及线程池的异常处理机制都有哪些?