您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Model Center   模型库  
会员   
   
基于 UML 和EA进行分析设计
7月30-31日 北京+线上
大模型核心技术RAG、MCP与智能体实践
8月14-15日 厦门
图数据库与知识图谱
8月21日-22日 北京+线上
     
   
 
 订阅
来聊聊守护线程和 JVM 的优雅关闭

 
作者:码农SharkChili

  30  次浏览      1 次
 2025-7-2
 
编辑推荐:
本文从虚拟机钩子、守护线程、finalize三个角度针对Java程序优雅关闭的哲学进行一些实践演示和建议。希望对你的学习有帮助。
本文来自于51CTO,由火龙果软件Linda编辑、推荐。

本文我们从虚拟机钩子、守护线程、finalize三个角度针对Java程序优雅关闭的哲学进行一些实践演示和建议,希望对你有帮助。

本文原本是针对守护线程的一些探讨,感觉知识点稍显浅薄,故基于原有文章进行迭代补充对于Java程序优雅关闭的一些思考。

一、JVM中的关闭

1. 详解虚拟机钩子

在Java进程开发中,对于重量级的系统资源关闭或者进程资源整理或信号输出,常常会通过Java内置的addShutdownHook方法注册回调函数,确保在Java进程关闭不再使用这些资源时将其释放,例如hutool这个工具类对应连接池的管理工具GlobalDSFactory,其底层就会在类加载初始化时利用addShutdownHook注册一个连接池销毁的回调函数:

/*
  * 设置在JVM关闭时关闭所有数据库连接
  */
 static {
  // JVM关闭时关闭所有连接池
  Runtime.getRuntime().addShutdownHook(new Thread() {
   @Override
   public void run() {
    if (null != factory) {
     factory.destroy();
     StaticLog.debug("DataSource: [{}] destroyed.", factory.dataSourceName);
     factory = null;
    }
   }
  });
 }

而虚拟机钩子注册的原理本质上就是在调用addShutdownHook时,其底层将这个现场hook注册到一个hooks的map容器中,并在shutdown的时候遍历调用这些hook线程:

对应的我们也给出addShutdownHook的实现,可以看到其底层就是调用ApplicationShutdownHooks来注册hook:

public void addShutdownHook(Thread hook) {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(new RuntimePermission("shutdownHooks"));
        }
        ApplicationShutdownHooks.add(hook);
    }

而步入这个add方法后可以看到其内部本质上就是在必要的校验后,存入到hooks这个map中:

private static IdentityHashMap<Thread, Thread> hooks;

static synchronized void add(Thread hook) {
        if(hooks == null)
            throw new IllegalStateException("Shutdown in progress");

        if (hook.isAlive())
            throw new IllegalArgumentException("Hook already running");

        if (hooks.containsKey(hook))
            throw new IllegalArgumentException("Hook previously registered");

        hooks.put(hook, hook);
    }

当触发虚拟机钩子关闭时,其内部就会针对hooks进行遍历并按照如下逻辑处理:

将hook线程启动,执行hook逻辑

调用join确保该hook能够准确执行完成

static void runHooks() {
        Collection<Thread> threads;
        synchronized(ApplicationShutdownHooks.class) {
            threads = hooks.keySet();
            hooks = null;
        }
  //遍历hook线程启动
        for (Thread hook : threads) {
            hook.start();
        }
        for (Thread hook : threads) {
            while (true) {
                try {
                //调用join加入主线程确保当前线程能够正确执行完成
                    hook.join();
                    break;
                } catch (InterruptedException ignored) {
                }
            }
        }
    }

当所有关闭钩子都执行结束时,如果runFinalizersOnExit为true,那么JVM就会运行终结器finalizers,此时JVM并不会停止或者关闭仍然在运行的应用线程。直到最终JVM结束,应用线程才会被关闭,对应的我们可以在源码Shutdown的exit方法印证:

static void exit(int status) {
        boolean runMoreFinalizers = false;
        synchronized (lock) {
             //......
            case FINALIZERS:
                if (status != 0) {
                    /* Halt immediately on nonzero status */
                    halt(status);
                } else {
                   //......
                   //将runFinalizersOnExit赋值给runMoreFinalizers 
                    runMoreFinalizers = runFinalizersOnExit;
                }
                break;
            }
        }
        //如果runMoreFinalizers 为true,则运行终结器
        if (runMoreFinalizers) {
            runAllFinalizers();
            halt(status);
        }
        //......
    }

2. 虚拟机钩子串行化使用

需要注意的虚拟机钩子注册后的调用时机,当JVM执行关闭钩子的时候,如果守护或者非守护线程也在运行,那么虚拟机钩子就可能和这些线程并发的执行,即虚拟机钩子可能会

并行的执行一些工作,所以对于一些存在依赖性的共享数据操作,虚拟机钩子要慎重使用。

例如我们用虚拟机钩子将日志服务关闭,此时如果另外的虚拟机钩子需要使用日志打印,可能就会报错:

例如我们的日志框架LogService ,本质上就是对于文件流的写入和关闭:

static class LogService {

        private static final BufferedWriter writer = FileUtil.getWriter("F:\\tmp\\log.txt", Charset.defaultCharset(), true);

        @SneakyThrows
        public void log(String msg) {//将数据写入日志中
            writer.write(msg);
        }


        public void close() {
            try {
                writer.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

如下图所说,若在虚拟机钩子上注册关闭打印和关闭日志框架的钩子,就有可能出现打印钩子抛出stream close的错误:

LogService logService = new LogService();



        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            //抛出stream close的错误
            logService.log("hello world");
        }));

        /**
         * 注册虚拟机钩子
         */
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            //执行一些应用程序的资源关闭
            logService.close();
        }))

总的来说,使用虚拟机钩子必须注意:

虚拟机钩子要保证线程安全,即针对共享资源做好同步把控

虚拟机钩子尽量串行化执行,且钩子之间不可以有任何依赖

关闭钩子应该尽快的退出,因为它直接的决定的JVM退出的结束时间

二、守护线程

1. 守护线程的基本概念

很多人对守护线程都不陌生,对于守护线程大部分读者都停留在JDK官方文档所介绍的概念:

The Java Virtual Machine exits when the only threads running are all daemon threads.

文档的意思是当JVM中不存在任何一个正在运行的非守护线程时,JVM进程会直接退出。

读起来很拗口对不对,没关系,本文就会基于几个代码示例,让你更深层次的理解守护线程。在此之前,读者不妨自测一下,下面这几道面试题:

守护线程和普通线程有什么区别?

守护线程默认优先级是多少?

若父线程为守护线程,在其内部创建一个普通线程,父线程停止,子线程是否也会停止呢?

如何创建守护线程池?

守护线程使用有哪些注意事项?

2. 守护线程和普通线程的区别

要了解区别就先来了解一下两者的使用,非守护线程,也就我们日常创建的普通线程,可以看到这段代码创建了一个普通线程,在无限循环的定时输出内容,而主线程仅仅是输出一段文字后就不做任何动作了。

public static void main(String[] args) {

        Thread t = new Thread(() -> {
            while (true) {
                log.info("普通线程执行了......");
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        t.start();
       log.info("主线程运行结束");


    }

对应的输出结果如下,可以看到,即使主线程停止运行了,而非守护线程也仍然会在运行,也就是JDK官方文档的字面含义,普通线程不停止,JVM就不停止运行:

12:44:57.022 [Thread-0] INFO com.sharkChili.webTemplate.Main - 普通线程执行了......
12:44:57.022 [main] INFO com.sharkChili.webTemplate.Main - 主线程运行结束
12:45:02.031 [Thread-0] INFO com.sharkChili.webTemplate.Main - 普通线程执行了.....

基于上述代码,用setDaemon(true)将该线程设置为守护线程:

public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (true) {
                log.info("守护线程执行了......");
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        //设置当前线程为守护线程
        t.setDaemon(true);
        t.start();
        log.info("主线程运行结束");
    }

输出结果如下,可以看到随着主线程的消亡,守护线程也会随之停止,不再运行,自此我相信读者可以理解JDK官方文档所说的那句话了,只要有一个普通线程在,JVM就不会退出,只要所有普通线程停止工作,JVM自动退出,守护线程也会自动结束。

12:44:23.239 [Thread-0] INFO com.sharkChili.webTemplate.Main - 守护线程执行了......
12:44:23.239 [main] INFO com.sharkChili.webTemplate.Main - 主线程运行结束

3. 守护线程和普通线程优先级的区别

我们可以通过getPriority方法查看两者的区别:

public static void main(String[] args) {

        Thread t = new Thread(() -> {

            log.info("守护线程优先级:{}", Thread.currentThread().getPriority());
        });

        //设置当前线程为守护线程
        t.setDaemon(true);
        t.start();
        log.info("主线程运行结束,当前线程运行优先级:{}", Thread.currentThread().getPriority());


    }

从输出结果来看,两者的优先级是一样的,都为5:

12:54:36.344 [main] INFO com.sharkChili.webTemplate.Main - 主线程运行结束,当前线程运行优先级:5
12:54:36.344 [Thread-0] INFO com.sharkChili.webTemplate.Main - 守护线程优先级:

4. 父守护线程问题

我们创建了一个守护线程,在其runnable实现中创建一个子线程:

public static void main(String[] args) {

        Thread parentThread = new Thread(() -> {
            Thread childThread = new Thread(() -> {
                while (true) {
                    log.info("子线程运行中,是否为守护线程:{}",Thread.currentThread().isDaemon());
                    try {
                        TimeUnit.HOURS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

            });

            childThread.start();

            log.info("parentThread守护线程运行中");
        });

        //设置当前线程为守护线程
        parentThread.setDaemon(true);
        parentThread.start();
        log.info("主线程运行结束");


    }

从输出结果来看,父线程为守护线程时,其内部创建的子线程也为守护线程,所以随着父线程的销毁,子线程也会同步销毁。

00:05:56.869 [Thread-1] INFO com.sharkChili.webTemplate.Main - 子线程运行中,是否为守护线程:true
00:05:56.869 [main] INFO com.sharkChili.webTemplate.Main - 主线程运行结束
00:05:56.869 [Thread-0] INFO com.sharkChili.webTemplate.Main - parentThread守护线程运行中

5. 守护线程池的创建

public static void main(String[] args) {

        ExecutorService threadPool = Executors.newFixedThreadPool(10, ThreadFactoryBuilder.create()
                .setNamePrefix("worker-")
                .setDaemon(true)
                .build());
        threadPool.execute(()->{
            while (true){
                try {
                    log.info("守护线程运行了");
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });


        log.info("主线程退出");


    }

6. 守护线程的使用场景

因为守护线程拥有自动结束自己生命周期的特性,当JVM中没有一个普通线程运行时,JVM会退出,即所有守护线程会自动停止,所以守护线程的使用场景可以有以下几种:

垃圾回收线程就是典型的守护线程,在后台进行垃圾对象回收的工作。

非核心业务工作可交由守护线程,例如:各类信息统计、服务监控等,一旦进程结束运行则这些守护线程停止工作。

7. 守护线程注意事项

复杂计算、资源回收这种不建议使用守护线程。

setDaemon要在start方法前面,否者该设置会不生效。

三、finalize关闭的哲学

1. 基本介绍

针对一些系统资源例如文件句柄或者套接字句柄,当不需要它们时,垃圾回收器定义了finalize方法进行一些资源关闭,一旦垃圾回收器回收这些对象之后,对应的资源就会调用finalize释放。

例如FileInputStream的finalize方法,它就会检查当前文件句柄是否非空,然后显示的调用一下close方法:

protected void finalize() throws IOException {
        if ((fd != null) &&  (fd != FileDescriptor.in)) {
           //关闭文件句柄
            close();
        }
    }

2. 终结器注意事项和正确资源关闭姿势

需要注意的finalize在JVM运行中可能会执行也可能不会执行,JVM对此无法做出保证,所以它运行时存着极端的不确定性,所以进行资源关闭时,我们非常不建议使用finalize。

正确的一些系统资源关闭回收,笔者更建议是使用阶段采用try-with-resource手动关闭资源:

//使用try-with-resource手动关闭资源
try(BufferedReader reader = FileUtil.getUtf8Reader("filePahth")){
            System.out.println(reader.readLine());
        }catch (Exception e){
            //异常处理
        }
 
   
30 次浏览       1
相关文章

Java微服务新生代之Nacos
深入理解Java中的容器
Java容器详解
Java代码质量检查工具及使用案例
相关文档

Java性能优化
Spring框架
SSM框架简单简绍
从零开始学java编程经典
相关课程

高性能Java编程与系统性能优化
JavaEE架构、 设计模式及性能调优
Java编程基础到应用开发
JAVA虚拟机原理剖析

最新活动计划
基于 UML 和EA进行分析设计 7-30[北京]
大模型RAG、MCP与智能体 8-14[厦门]
软件架构设计方法、案例与实践 7-24[北京]
用户体验、易用性测试与评估 7-25[西安]
图数据库与知识图谱 8-23[北京]
需求分析师能力培养 8-28[北京]
 
 
最新文章
Java虚拟机架构
JVM——Java虚拟机架构
Java容器详解
Java进阶--深入理解ArrayList实现原理
Java并发容器,底层原理深入分析
最新课程
java编程基础到应用开发
JavaEE架构、 设计模式及性能调优
高性能Java编程与系统性能优化
SpringBoot&Cloud、JavaSSM框架
Spring Boot 培训
更多...   
成功案例
国内知名银行 Spring+SpringBoot+Cloud+MVC
北京 Java编程基础与网页开发基础
北京 Struts+Spring
华夏基金 ActiveMQ 原理
某民航公 Java基础编程到应用开发
更多...