1、捕获线程运行时异常
Thread 类中,关于处理运行时异常的 API 总共有四个,如下:
// 为某个特定线程指定 UncaughtExceptionHandlerpublic void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) {checkAccess();uncaughtExceptionHandler = eh;}// 设置全局的 UncaughtExceptionHandlerpublic static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) {SecurityManager sm = System.getSecurityManager();if (sm != null) {sm.checkPermission(new RuntimePermission("setDefaultUncaughtExceptionHandler"));}defaultUncaughtExceptionHandler = eh;}// 获取特定线程的 UncaughtExceptionHandlerpublic UncaughtExceptionHandler getUncaughtExceptionHandler() {return uncaughtExceptionHandler != null ?uncaughtExceptionHandler : group;}// 获取全局的 UncaughtExceptionHandlerpublic static UncaughtExceptionHandler getDefaultUncaughtExceptionHandler(){return defaultUncaughtExceptionHandler;}
1.1、UncaughtExceptionHandler 介绍
线程在执行单元中是不允许抛出 checked 异常的,而且线程运行在自己的上下文中,派生它的线程将无法直接获得它运行中出现的异常信息。对此,Java 为我们提供了一个 UncaughtExceptionHandler 接口,当线程在运行过程中出现异常时,会调用 UncaughtExceptionHandler 接口,从而得知是哪个线程在运行时出错,以及出现了什么样的错误。 Thread 中 UncaughtExceptionHandler 的源码:
@FunctionalInterfacepublic 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 是一个FunctionalInterface(该注解只能标记在**的接口上) 接口,只有一个抽象方法。该回调接口会被 Thread 中的 dispatchUncaughtException 发方法调用,代码如下。当线程在运行过程中出现异常时,JVM 会调用 dispatchUncaughtException 方法,该方法会将对应的线程实例以及异常信息传递给回调接口。
/*** Dispatch an uncaught exception to the handler. This method is* intended to be called only by the JVM.*/private void dispatchUncaughtException(Throwable e) {getUncaughtExceptionHandler().uncaughtException(this, e);}
1.2、UncaughtExceptionHandler 实例
示例代码:
package com.yj.thread_group;import java.util.concurrent.TimeUnit;/*** @description: UncaughtExceptionHandler 实例* @author: erlang* @since: 2021-03-01 22:02*/public class ThreadUncaughtExceptionHandler {public static void main(String[] args) {// 设置回调接口Thread.setDefaultUncaughtExceptionHandler((t, e) -> {System.out.println(t.getName() + " throw exception");e.printStackTrace();});new Thread(() -> {try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {}System.out.println(1 / 0);}, "Thread-Demo").start();}}
执行结果:
Thread-Demo throw exceptionjava.lang.ArithmeticException: / by zeroat com.yj.thread_group.UncaughtExceptionHandlerDemo.lambda$main$1(UncaughtExceptionHandlerDemo.java:26)at java.lang.Thread.run(Thread.java:748)
1.3、UncaughtExceptionHandler 源码
在没有向线程注入 UncaughtExceptionHandler 回调接口的情况时,线程若出现了异常有将如何处理呢?从 getUncaughtExceptionHandler 方法可以看出,该方法首先会判断当前线程是否设置了 handler?如果有则执行线程自己实现的 uncaughtException 方法,否则就到所在的 ThreadGroup 中获取。
// 获取特定线程的 UncaughtExceptionHandlerpublic UncaughtExceptionHandler getUncaughtExceptionHandler() {return uncaughtExceptionHandler != null ?uncaughtExceptionHandler : group;}
ThreadGroup 中 UncaughtExceptionHandler 接口的实现,代码如下:
- 如果该线程组有父线程组,则直接调用父线程组的 uncaughtException 方法;
- 如果设置了全局默认的 UncaughtExceptionHandler,则调用该 Handler 的 uncaughtException 方法
- 如果既没有父线程组,也没有设置全局线默认的 UncaughtExceptionHandler,则会直接将异常的堆栈信息定向到 System.err 中。
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);}}}
2、注入钩子线程
2.1、Hook 线程
JVM 进程的退出是由于 JVM 进程汇中没有活跃的非守护线程,或者收到了系统中断信号,向 JVM 程序注入一个 Hook 线程,在 JVM 进程退出的时候,Hook 线程会启动执行,通过 Runtime 可以为 JVM 注入多个线程。示例代码:
package com.yj.thread_group;import java.util.concurrent.TimeUnit;/*** @description: 钩子线程* @author: erlang* @since: 2021-03-02 22:07*/public class HookThread {public static void main(String[] args) {Runtime.getRuntime().addShutdownHook(new Thread(() -> {System.out.println("The hook thread1 is start.");try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("The hook thread1 is end.");}));Runtime.getRuntime().addShutdownHook(new Thread(() -> {System.out.println("The hook thread2 is start.");try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("The hook thread2 is end.");}));System.out.println("main is end");}}
上述代码中,给 Java 程序注入了两个 Hook 线程,在 main 线程中结束,即 JVM 中没有了活动的守护线程,JVM 进程即将退出时,两个 Hook 线程会启动运行。执行结果:
main is endThe hook thread2 is start.The hook thread1 is start.The hook thread2 is end.The hook thread1 is end.
2.2、Hook 线程实战
开发中经常遇到 Hook 线程,比如为了防止某个程序被重复启动,在进程启动时会创建一个 lock 文件;在进程收到中断信号时,会删除 lock 文件。在 mysql、zookeeper 等系统中都能看到这个 lock 文件。示例代码:
package com.yj.thread_group;import java.io.IOException;import java.nio.file.Files;import java.nio.file.Path;import java.nio.file.Paths;import java.nio.file.attribute.PosixFilePermission;import java.nio.file.attribute.PosixFilePermissions;import java.util.Set;import java.util.concurrent.TimeUnit;/*** @description: 启动时钩子,防止线程多次启动* @author: erlang* @since: 2021-03-02 22:26*/public class StartupHook {private final static String LOCK_PATH = "src/main/java/locks";private final static String LOCK_FILE = ".lock";private final static String PERMISSION = "rwxr-xr-x";public static void main(String[] args) throws InterruptedException, IOException {// 检查是否存在 .lock 文件checkRunning();// 注入 Hook 线程,在程序退出时删除 lock 文件Runtime.getRuntime().addShutdownHook(new Thread(() -> {System.out.println("\nThe hook is start.");getLockFile().toFile().delete();}));// 模拟程序启动中System.out.println("running ");for (;;) {TimeUnit.SECONDS.sleep(1);System.out.print("#");}}private static Path getLockFile() {return Paths.get(LOCK_PATH, LOCK_FILE);}private static void checkRunning() throws IOException {Path path = getLockFile();if (path.toFile().exists()) {throw new RuntimeException("The program already running.");}Set<PosixFilePermission> perms = PosixFilePermissions.fromString(PERMISSION);Files.createFile(path, PosixFilePermissions.asFileAttribute(perms));}}
运行上面的程序,会发现 LOCK_PATH 目录下多了一个 .lock 文件,如图所示。

终止程序时,JVM 进程会收到中断信号,并启动 hook 线程,删除 .lock 文件,执行结果如下:
running######The hook is start.
3、Hook 线程应用场景
Hook 线程只有在收到退出信号的时候会被执行,如果在 kill 的时候使用了参数 -9,那么 Hook 线程不会得到执行,进程将会立即退出,.lock 文件将不会被删除。Hook 线程中也可以执行一些释放资源的工作,比如关闭文件句柄、socket 链接、数据库链接等。尽量不要在 Hook 线程中执行一些耗时非常长的操作,因为这会导致程序迟迟不能退出
4、总结
本篇介绍了如何通过 Handler 回调的方式获取线程运行期间的异常信息。Hook 线程是一个非常好的机制,可以帮助程序获得进程的中断信号,有机会在进程退出之前做一些资源释放的动作,或者做一些告警通知。如果是强制杀死进程(kill -9),那么进程将不会收到任何中断信号。
