Java 多线程之 Hook (钩子) 线程

通常情况下,我们可以向应用程序注入一个或多个 Hook (钩子) 线程,这样,在程序即将退出的时候,也就是 JVM 程序即将退出的时候,Hook 线程就会被启动执行。

先看一段示例代码:

public class AnswerApp {

    public static void main(String[] args) {
        // 为应用程序注册 Hook(钩子) 线程
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            try {
                System.out.println("The hook thread-1 is running...");
                // 休眠2秒
                TimeUnit.SECONDS.sleep(2);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("The hook thread-1 will exit.");
        }));

        // 再注册一个 Hook(钩子) 线程
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            try {
                System.out.println("The hook thread-2 is running...");
                // 休眠2秒
                TimeUnit.SECONDS.sleep(2);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("The hook thread-2 will exit.");
        }));

        System.out.println("The main thread will exit.");
    }

}
  • 为应用程序注入一个钩子(Hook)线程,线程中,打印了相关日志,包括正在运行以及退出的日志;
  • 再次注入一个同样逻辑的钩子(Hook)线程;
  • 主线程执行结束,打印日志;

运行这段代码,来验证一下:

The main thread will exit.
The hook thread-2 is running...
The hook thread-1 is running...
The hook thread-2 will exit.
The hook thread-1 will exit.

从打印日志看到,当主线程执行结束,也就是 JVM 进程即将退出的时候,注入的两个 Hook 线程都被启动并打印相关日志。

 

Hook 线程的应用场景&注意事项

应用场景

上面我们已经知道了, Hook 线程能够在 JVM 程序退出的时候被启动且执行,那么,我们能够通过这种特性,做点什么呢?

罗列一些常见应用场景:

  • 防止程序重复执行,具体实现可以在程序启动时,校验是否已经生成 lock 文件,如果已经生成,则退出程序,如果未生成,则生成 lock 文件,程序正常执行,最后再注入 Hook 线程,这样在 JVM 退出的时候,线程中再将 lock 文件删除掉;

PS: 这种防止程序重复执行的策略,也被应用于 Mysql 服务器,zookeeper, kafka 等系统中。

  • Hook 线程中也可以执行一些资源释放的操作,比如关闭数据库连接,Socket 连接等。

注意事项

  • Hook 线程只有在正确接收到退出信号时,才能被正确执行,如果你是通过 kill -9这种方式,强制杀死的进程,那么抱歉,进程是不会去执行 Hook 线程的,为什么呢?你想啊,它自己都被强制干掉了,哪里还管的上别人呢?但是如果通过kill pid来杀死进程,Hook线程就会执行

  • 请不要在 Hook 线程中执行一些耗时的操作,这样会导致程序长时间不能退出。

 

Hook 线程防应用重启实战

针对上面防应用重启的场景,利用 Hook 线程,我们来实战一下,贴上代码:

public class PreventDuplicated {
    /** .lock 文件存放路径 */
    private static final String LOCK_FILE_PATH = "./";
    /** .lock 文件名称 */
    private static final String LOCK_FILE_NAME = ".lock";

    public static void main(String[] args) throws Exception {
        // 校验 .lock 文件是否已经存在
        checkLockFile();

        // 注入 Hook 线程
        addShutdownHook();

        // 模拟程序一直运行
        while (true) {
            TimeUnit.SECONDS.sleep(1);
            System.out.println("The program is running...");
        }
    }


    /**
     * 注入 Hook 线程
     */
    private static void addShutdownHook() {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            // 接受到了退出信号
            System.out.println("The program received kill signal.");
            // 删除 .lock 文件
            deleteLockFile();
        }));
    }

    /**
     * 校验 .lock 文件是否已经存在
     */
    private static void checkLockFile() {
        if (isLockFileExisted()) {
            // .lock 文件已存在, 抛出异常, 退出程序
            throw new RuntimeException("The program already running.");
        }

        // 不存在,则创建 .lock 文件
        createLockFile();
    }

    /**
     * 创建 .lock 文件
     */
    private static void createLockFile() {
        File file = new File(LOCK_FILE_PATH + LOCK_FILE_NAME);
        try {
            boolean newFile = file.createNewFile();
            if (!newFile) {
                throw new RuntimeException("create lock file exception.");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * .lock 文件 是否存在
     */
    private static boolean isLockFileExisted() {
        File file = new File(LOCK_FILE_PATH + LOCK_FILE_NAME);
        return file.exists();
    }

    /**
     * 删除 .lock 文件
     */
    private static void deleteLockFile() {
        File file = new File(LOCK_FILE_PATH + LOCK_FILE_NAME);
        boolean delete = file.delete();
        if (!delete) {
            throw new RuntimeException("delete lock file exception.");
        }
    }
}

你可能感兴趣的:(并发编程,开发笔记)