7.Hook线程及捕获线程异常

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

  • public void setUncaughtExcetionHandler(UncaughtExceptionHandler eh) 为某个特定线程指定UnCaughtExceptionHandler
  • public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) 设置全局的UnCaughtExceptionHandler
  • public UncaughtExceptionHandler getUncaughtExceptionHandler() 获取特定线程的UncaughtExceptionHandler
  • public static UncaughtExceptionHandler getDefaultUncaughtExceptionHandler() 获取全局的UncaughtExceptionHandler

UncaughtExceptionHandler介绍

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

package com.shinley.concurrent.chapter7;

import java.util.concurrent.TimeUnit;

public class CaptureThreadException {
    public static void main(String[] args) {
        Thread.setDefaultUncaughtExceptionHandler((thread, e) -> {
            System.out.println(thread.getName() + " occur exception");
            e.printStackTrace();
        });

        final Thread thread = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {

            }

            // 抛出异常
            System.out.println(1/0);

        }, "Test-Thread");
        thread.start();
    }
}

打印

Test-Thread occur exception
java.lang.ArithmeticException: / by zero
	at com.shinley.concurrent.chapter7.CaptureThreadException.lambda$main$1(CaptureThreadException.java:20)
	at java.lang.Thread.run(Thread.java:748)

注入钩子线程

Jvm进程的退出是由于JVM进程中没有活跃的非守护线程,或者收到了系统中断信号, 向JVM程序注入一个Hook线程, JVM进程退出的时候, Hook线程会启动执行。

通过Runtime可以为JVM注入多个Hook线程。

package com.shinley.concurrent.chapter7;

import java.util.concurrent.TimeUnit;

public class ThreadHook {
    public static void main(String[] args) {
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                System.out.println("the hook thread 1 is running");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 可以多次注入
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                try {
                    System.out.println("the hook thread 2 is running");
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        System.out.println("The application will be stopped");
    }
}

打印

The application will be stopped
the hook thread 2 is running
the hook thread 1 is running

Hook线程实战

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

package com.shinley.concurrent.chapter7;

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;

public class PreventDuplicated {

    private final static String LOCK_PATH = System.getProperty("user.home");

    private final static  String LOCK_FILE = ".lick";
    private final static String PERMISSIONS = "rw------";

    public static void main(String[] args) throws IOException {
       Runtime.getRuntime().addShutdownHook(new Thread(() -> {
           System.out.println("the application received kill SIGNAL");
           getLockFile().toFile().delete();
       }));

       checkRunning();

       for (;;) {
           try {
               TimeUnit.MILLISECONDS.sleep(1);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       }
    }

    private static void checkRunning() throws IOException {
        Path path = getLockFile();
        if (path.toFile().exists()) {
            throw new RuntimeException("The application already running");
        }
        Set<PosixFilePermission> perms = PosixFilePermissions.fromString(PERMISSIONS);

        Files.createFile(path, PosixFilePermissions.asFileAttribute(perms));
    }

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