1、捕获线程运行时异常

Thread 类中,关于处理运行时异常的 API 总共有四个,如下:

  1. // 为某个特定线程指定 UncaughtExceptionHandler
  2. public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
  3. checkAccess();
  4. uncaughtExceptionHandler = eh;
  5. }
  6. // 设置全局的 UncaughtExceptionHandler
  7. public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
  8. SecurityManager sm = System.getSecurityManager();
  9. if (sm != null) {
  10. sm.checkPermission(
  11. new RuntimePermission("setDefaultUncaughtExceptionHandler")
  12. );
  13. }
  14. defaultUncaughtExceptionHandler = eh;
  15. }
  16. // 获取特定线程的 UncaughtExceptionHandler
  17. public UncaughtExceptionHandler getUncaughtExceptionHandler() {
  18. return uncaughtExceptionHandler != null ?
  19. uncaughtExceptionHandler : group;
  20. }
  21. // 获取全局的 UncaughtExceptionHandler
  22. public static UncaughtExceptionHandler getDefaultUncaughtExceptionHandler(){
  23. return defaultUncaughtExceptionHandler;
  24. }

1.1、UncaughtExceptionHandler 介绍

线程在执行单元中是不允许抛出 checked 异常的,而且线程运行在自己的上下文中,派生它的线程将无法直接获得它运行中出现的异常信息。对此,Java 为我们提供了一个 UncaughtExceptionHandler 接口,当线程在运行过程中出现异常时,会调用 UncaughtExceptionHandler 接口,从而得知是哪个线程在运行时出错,以及出现了什么样的错误。 Thread 中 UncaughtExceptionHandler 的源码:

  1. @FunctionalInterface
  2. public interface UncaughtExceptionHandler {
  3. /**
  4. * Method invoked when the given thread terminates due to the
  5. * given uncaught exception.
  6. * <p>Any exception thrown by this method will be ignored by the
  7. * Java Virtual Machine.
  8. * @param t the thread
  9. * @param e the exception
  10. */
  11. void uncaughtException(Thread t, Throwable e);
  12. }

从上面源码中可以看出 UncaughtExceptionHandler 是一个FunctionalInterface(该注解只能标记在**的接口上) 接口,只有一个抽象方法。该回调接口会被 Thread 中的 dispatchUncaughtException 发方法调用,代码如下。当线程在运行过程中出现异常时,JVM 会调用 dispatchUncaughtException 方法,该方法会将对应的线程实例以及异常信息传递给回调接口。

  1. /**
  2. * Dispatch an uncaught exception to the handler. This method is
  3. * intended to be called only by the JVM.
  4. */
  5. private void dispatchUncaughtException(Throwable e) {
  6. getUncaughtExceptionHandler().uncaughtException(this, e);
  7. }

1.2、UncaughtExceptionHandler 实例

示例代码:

  1. package com.yj.thread_group;
  2. import java.util.concurrent.TimeUnit;
  3. /**
  4. * @description: UncaughtExceptionHandler 实例
  5. * @author: erlang
  6. * @since: 2021-03-01 22:02
  7. */
  8. public class ThreadUncaughtExceptionHandler {
  9. public static void main(String[] args) {
  10. // 设置回调接口
  11. Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
  12. System.out.println(t.getName() + " throw exception");
  13. e.printStackTrace();
  14. });
  15. new Thread(() -> {
  16. try {
  17. TimeUnit.SECONDS.sleep(2);
  18. } catch (InterruptedException e) {
  19. }
  20. System.out.println(1 / 0);
  21. }, "Thread-Demo").start();
  22. }
  23. }

执行结果:

  1. Thread-Demo throw exception
  2. java.lang.ArithmeticException: / by zero
  3. at com.yj.thread_group.UncaughtExceptionHandlerDemo.lambda$main$1(UncaughtExceptionHandlerDemo.java:26)
  4. at java.lang.Thread.run(Thread.java:748)

1.3、UncaughtExceptionHandler 源码

在没有向线程注入 UncaughtExceptionHandler 回调接口的情况时,线程若出现了异常有将如何处理呢?从 getUncaughtExceptionHandler 方法可以看出,该方法首先会判断当前线程是否设置了 handler?如果有则执行线程自己实现的 uncaughtException 方法,否则就到所在的 ThreadGroup 中获取。

  1. // 获取特定线程的 UncaughtExceptionHandler
  2. public UncaughtExceptionHandler getUncaughtExceptionHandler() {
  3. return uncaughtExceptionHandler != null ?
  4. uncaughtExceptionHandler : group;
  5. }

ThreadGroup 中 UncaughtExceptionHandler 接口的实现,代码如下:

  1. 如果该线程组有父线程组,则直接调用父线程组的 uncaughtException 方法;
  2. 如果设置了全局默认的 UncaughtExceptionHandler,则调用该 Handler 的 uncaughtException 方法
  3. 如果既没有父线程组,也没有设置全局线默认的 UncaughtExceptionHandler,则会直接将异常的堆栈信息定向到 System.err 中。
  1. public void uncaughtException(Thread t, Throwable e) {
  2. if (parent != null) {
  3. parent.uncaughtException(t, e);
  4. } else {
  5. Thread.UncaughtExceptionHandler ueh =
  6. Thread.getDefaultUncaughtExceptionHandler();
  7. if (ueh != null) {
  8. ueh.uncaughtException(t, e);
  9. } else if (!(e instanceof ThreadDeath)) {
  10. System.err.print("Exception in thread \""
  11. + t.getName() + "\" ");
  12. e.printStackTrace(System.err);
  13. }
  14. }
  15. }

2、注入钩子线程

2.1、Hook 线程

JVM 进程的退出是由于 JVM 进程汇中没有活跃的非守护线程,或者收到了系统中断信号,向 JVM 程序注入一个 Hook 线程,在 JVM 进程退出的时候,Hook 线程会启动执行,通过 Runtime 可以为 JVM 注入多个线程。示例代码:

  1. package com.yj.thread_group;
  2. import java.util.concurrent.TimeUnit;
  3. /**
  4. * @description: 钩子线程
  5. * @author: erlang
  6. * @since: 2021-03-02 22:07
  7. */
  8. public class HookThread {
  9. public static void main(String[] args) {
  10. Runtime.getRuntime().addShutdownHook(new Thread(() -> {
  11. System.out.println("The hook thread1 is start.");
  12. try {
  13. TimeUnit.SECONDS.sleep(2);
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. System.out.println("The hook thread1 is end.");
  18. }));
  19. Runtime.getRuntime().addShutdownHook(new Thread(() -> {
  20. System.out.println("The hook thread2 is start.");
  21. try {
  22. TimeUnit.SECONDS.sleep(2);
  23. } catch (InterruptedException e) {
  24. e.printStackTrace();
  25. }
  26. System.out.println("The hook thread2 is end.");
  27. }));
  28. System.out.println("main is end");
  29. }
  30. }

上述代码中,给 Java 程序注入了两个 Hook 线程,在 main 线程中结束,即 JVM 中没有了活动的守护线程,JVM 进程即将退出时,两个 Hook 线程会启动运行。执行结果:

  1. main is end
  2. The hook thread2 is start.
  3. The hook thread1 is start.
  4. The hook thread2 is end.
  5. The hook thread1 is end.

2.2、Hook 线程实战

开发中经常遇到 Hook 线程,比如为了防止某个程序被重复启动,在进程启动时会创建一个 lock 文件;在进程收到中断信号时,会删除 lock 文件。在 mysql、zookeeper 等系统中都能看到这个 lock 文件。示例代码:

  1. package com.yj.thread_group;
  2. import java.io.IOException;
  3. import java.nio.file.Files;
  4. import java.nio.file.Path;
  5. import java.nio.file.Paths;
  6. import java.nio.file.attribute.PosixFilePermission;
  7. import java.nio.file.attribute.PosixFilePermissions;
  8. import java.util.Set;
  9. import java.util.concurrent.TimeUnit;
  10. /**
  11. * @description: 启动时钩子,防止线程多次启动
  12. * @author: erlang
  13. * @since: 2021-03-02 22:26
  14. */
  15. public class StartupHook {
  16. private final static String LOCK_PATH = "src/main/java/locks";
  17. private final static String LOCK_FILE = ".lock";
  18. private final static String PERMISSION = "rwxr-xr-x";
  19. public static void main(String[] args) throws InterruptedException, IOException {
  20. // 检查是否存在 .lock 文件
  21. checkRunning();
  22. // 注入 Hook 线程,在程序退出时删除 lock 文件
  23. Runtime.getRuntime().addShutdownHook(new Thread(() -> {
  24. System.out.println("\nThe hook is start.");
  25. getLockFile().toFile().delete();
  26. }));
  27. // 模拟程序启动中
  28. System.out.println("running ");
  29. for (;;) {
  30. TimeUnit.SECONDS.sleep(1);
  31. System.out.print("#");
  32. }
  33. }
  34. private static Path getLockFile() {
  35. return Paths.get(LOCK_PATH, LOCK_FILE);
  36. }
  37. private static void checkRunning() throws IOException {
  38. Path path = getLockFile();
  39. if (path.toFile().exists()) {
  40. throw new RuntimeException("The program already running.");
  41. }
  42. Set<PosixFilePermission> perms = PosixFilePermissions.fromString(PERMISSION);
  43. Files.createFile(path, PosixFilePermissions.asFileAttribute(perms));
  44. }
  45. }

运行上面的程序,会发现 LOCK_PATH 目录下多了一个 .lock 文件,如图所示。

image.png

终止程序时,JVM 进程会收到中断信号,并启动 hook 线程,删除 .lock 文件,执行结果如下:

  1. running
  2. ######
  3. The hook is start.

3、Hook 线程应用场景

Hook 线程只有在收到退出信号的时候会被执行,如果在 kill 的时候使用了参数 -9,那么 Hook 线程不会得到执行,进程将会立即退出,.lock 文件将不会被删除。Hook 线程中也可以执行一些释放资源的工作,比如关闭文件句柄、socket 链接、数据库链接等。尽量不要在 Hook 线程中执行一些耗时非常长的操作,因为这会导致程序迟迟不能退出

4、总结

本篇介绍了如何通过 Handler 回调的方式获取线程运行期间的异常信息。Hook 线程是一个非常好的机制,可以帮助程序获得进程的中断信号,有机会在进程退出之前做一些资源释放的动作,或者做一些告警通知。如果是强制杀死进程(kill -9),那么进程将不会收到任何中断信号。