1.获取线程运行时异常

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

  1. public void set UncaughtExceptionHandler(UncaughtExceptionHandler eh)
  2. //为某个特定线程指定UncaughtExceptionHandler。
  3. public static void set DefaultUncaughtExceptionHandler(UncaughtExceptionHandler
  4. eh)
  5. //设置全局的UncaughtExceptionHandler。
  6. public UncaughtExceptionHandler getUncaughtExceptionHandler()
  7. //获取特定线程的UncaughtExceptionHandler。

1.1 UncaughtExceptionHandler的介绍

线程在执行单元中是不允许抛出checked异常的,这一点前文中已经有过交代,而且线 程运行在自己的上下文中,派生它的线程将无法直接获得它运行中出现的异常信息。对此,Java为我们提供了一个UncaughtExceptionHandler接口,当会回线程在运行调Uncaught过程中出Exception现异常Handler时,接口, 从而得知是哪个线程在运行时出错,以及出现了什么样的错误,示例代码如 下:

  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::uncaughtException方法会被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. }

当线程在运行过程中出现异常时,JVM会调用dispatchUncaughtException方法,该方法会将对应的线程实例以及异常信息传递给回调接口。

1.2 UncaughtExceptionHandler实例

  1. import java.util.concurrent.TimeUnit;
  2. public class TestThreadException {
  3. public static void main(String[] args) {
  4. Thread.setDefaultUncaughtExceptionHandler(
  5. ((t, e) -> {
  6. System.out.println(t.getName() + " occur exception");
  7. e.printStackTrace();
  8. })
  9. );
  10. final Thread thread = new Thread(
  11. () -> {
  12. try {
  13. TimeUnit.SECONDS.sleep(2);
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. System.out.println(1/0);
  18. }
  19. ,"Test-Thread");
  20. thread.start();
  21. }
  22. }

程序运行2秒之后,抛出异常
image.png

1.3 UncaughtExceptionHandler 源码分析

  1. public UncaughtExceptionHandler getUncaughtExceptionHandler() {
  2. return uncaughtExceptionHandler != null ?
  3. uncaughtExceptionHandler : group;
  4. }

getUncaughtExceptionHandler方法首先会判断当前线程是否设置了handler, 如果有则执行线程自己的uncaught Exception方法, 否则就到所在的ThreadGroup中获取,ThreadGroup同样也实现了UncaughtExceptionHandler接口, 下面再来看看Thread Group的uncaughtException方法。

  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. }

该ThreadGroup如果有父ThreadGroup, 则直接调用父Group的uncaughtException方法。如果设置了全局默认的UncaughtExceptionHandler, 则调用uncaughtException方法。若既没有父ThreadGroup,也没有设置全局默认的UncaughtExceptionHandler, 则会直接将异常的堆栈信息定向到System.err中。

2.注入钩子线程

2.1Hook线程介绍

JVM进程的退出是由于JVM进程中没有活跃的非守护线程, 或者收到了系统中断信号, 向JVM程序注入一个Hook线程, 在JVM进程退出的时候, Hook线程会启动执行,通过Runtime可以为JVM注入多个Hook线程, 下面就通过一个简单的例子来看一下如何向Java程序注入Hook线程。

  1. import java.util.concurrent.TimeUnit;
  2. public class TestThreadHook {
  3. public static void main(String[] args) {
  4. Runtime.getRuntime().addShutdownHook(
  5. new Thread() {
  6. @Override
  7. public void run() {
  8. try {
  9. System.out.println("The hook thread 1 is running");
  10. TimeUnit.SECONDS.sleep(1);
  11. } catch (InterruptedException e) {
  12. e.printStackTrace();
  13. }
  14. System.out.println("The hook thread 1 whill exit");
  15. }
  16. }
  17. );
  18. Runtime.getRuntime().addShutdownHook(
  19. new Thread() {
  20. @Override
  21. public void run() {
  22. try {
  23. System.out.println("The hook thread 2 is running");
  24. TimeUnit.SECONDS.sleep(1);
  25. } catch (InterruptedException e) {
  26. e.printStackTrace();
  27. }
  28. System.out.println("The hook thread 2 will exit");
  29. }
  30. }
  31. );
  32. System.out.println("The program will is stopping");
  33. }
  34. }

在代码中, 给Java程序注人了两个Hook线程, 在main线程中结束, 也就是JVM中没有了活动的非守护线程, JVM进程即将退出时, 两个Hook线程会被启动并且运行,输出结果如下:
image.png

2.2Hook线程实战

在我们的开发中经常会遇到Hook线程, 比如为了防止某个程序被重复启动, 在进程启动时会创建一个lock文件, 进程收到中断信号的时候会删除这个lock文件, 我们在mysql服务器、zookeeper、kafka等系统中都能看到lock文件的存在, 本节中, 将利用hook线程的特点,模拟一个防止重复启动的程序,如代码所示。

  1. import java.io.IOException;
  2. import java.nio.file.Files;
  3. import java.nio.file.Path;
  4. import java.nio.file.Paths;
  5. import java.nio.file.attribute.PosixFilePermission;
  6. import java.nio.file.attribute.PosixFilePermissions;
  7. import java.util.Set;
  8. import java.util.concurrent.TimeUnit;
  9. public class PreventDuplicated {
  10. private final static String LOCK_PATH="D:\\locks";
  11. private final static String LOCK_FILE=".lock";
  12. private final static String PERMISSIONS="rw-------";
  13. public static void main(String[] args) throws IOException {
  14. // 注入Hook线程,在程序退出时删除lock文件
  15. Runtime.getRuntime().addShutdownHook(
  16. new Thread(
  17. ()-> {
  18. System.out.println("Thre program received kill SIGNAL");
  19. }
  20. )
  21. );
  22. // 检查是否存在.lock文件
  23. checkRunning();
  24. // 简答模拟当前程序正在运行
  25. for(;;) {
  26. try {
  27. TimeUnit.MILLISECONDS.sleep(1);
  28. System.out.println("program is running");
  29. } catch (InterruptedException e) {
  30. e.printStackTrace();
  31. }
  32. }
  33. }
  34. private static void checkRunning() throws IOException {
  35. Path path = getLockFile();
  36. if ( path.toFile().exists()) {
  37. throw new RuntimeException("The program already running");
  38. }
  39. Set<PosixFilePermission> perms = PosixFilePermissions.fromString(PERMISSIONS);
  40. Files.createFile(path, PosixFilePermissions.asFileAttribute(perms));
  41. }
  42. private static Path getLockFile() {
  43. return Paths.get(LOCK_PATH, LOCK_FILE);
  44. }
  45. }

2.3 Hook线程应用场景以及注意事项

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