从JVM的退出机制分析Java程序的优雅关闭退出

news2024/9/24 21:21:59

前言

Java程序启动从main函数开始启动,是程序入口和主线程,但程序会在什么时候结束?为什么有的Java程序在启动后很快就结束了,比如HelloWorld程序,有的程序却能一直在运行,比如Tomcat启动后就一直保持进程不关闭。使用kill -15 PID关闭JVM进程究竟有没有问题?为了搞清楚这些问题,本文来详细分析一下JVM退出的机制。

JVM的退出介绍

JVM的退出可以分正常退出、异常退出和强制退出,每种退出方法的不同会产生不过的情况,汇总如下:

在这里插入图片描述

Linux操作系统关闭不完全支持优雅退出,原因是Linux关闭时先会向进程发送SIGTERM信号,等待一段时间进程还没退出时就会强制关闭进程,所以Linux只会给一定时间让进程关闭退出。

JVM可通过以下几种方式正常退出

  • 最后一个非守护线程结束。
  • JVM被中断(通过ctrl + c或发送SIGINT信号)。
  • JVM被终止(通过发送SIGTERM信号,即kill -15 PIDkill PID)。
  • 某个线程调用System.exit()Runtime.exit()

System.exit(int)被调用时,会通过security manager进行检查,是否允许以给定的状态退出,当允许时会调用Shutdown.exit()

当向JVM发送中断信号(SIGINT)或终止信号(SIGTERM),不经过security manager检查,直接调用Shutdown.exit()

Shutdown类的exit()会运行Shutdown Hook,通过一个锁防止这些Hook执行两次,在最后会调用halt(int)真正的去关闭JVM。

为什么SIGKILL(kill -9)无法实现应用的优雅关闭

SIGKILL(使用 kill -9 命令发送)无法实现应用的优雅关闭,因为它是一种无条件的终止信号,会立即终止目标进程,而不给进程执行任何清理或收尾工作的机会。这包括关闭文件、释放资源、保存状态等。简而言之,SIGKILL不会让进程有机会进行任何“优雅”的关闭操作。

相反,常规的进程终止信号 SIGTERM 允许进程执行清理工作。当你发送 SIGTERM 信号时,进程会收到这个信号并可以自行决定如何处理它,比如关闭文件、释放资源、保存状态等,然后正常退出。这种方式更为优雅,因为它给了应用程序执行关闭过程的机会。

ShutdownHook

在具体分析JVM的每种退出方式之前先来了解一下与退出机制息息相关的概念:ShutdownHook(关闭钩子)。

ShutdownHook(关闭钩子)是一个已经初始化但尚未启动的线程。当虚拟机开始其关闭步骤时,它会以某种未指定的顺序启动所有已注册的关闭钩子,并让它们并发运行。当所有钩子都完成后,如果启用了退出时的清理(finalization-on-exit),那么它会运行所有尚未调用的终结器。最后,虚拟机将停止。关闭钩子应该尽快完成它们的工作。当一个程序调用exit时,期望是虚拟机会迅速关闭并退出。当虚拟机因外部因素(如用户中断或系统事件)而终止时,关闭钩子提供了一个机会来执行一些清理工作或保存状态,但同样应该尽快完成。

以下是一个简单的Shutdown Hook栗子:

public class SimpleShutdownHookTest {

    public static void main(String[] args) {

        MyHook myHook1 = new MyHook("hook-1");
        MyHook myHook2 = new MyHook("hook-2");

        Runtime.getRuntime().addShutdownHook(myHook1);
        Runtime.getRuntime().addShutdownHook(myHook2);

        System.exit(0);  //系统退出,会启动Shutdown Hook线程
    }

    static class MyHook extends Thread {

        public MyHook(String name) {
            super.setName(name);
        }

        public void run() {
            try {
                System.out.println("do shutdown " + Thread.currentThread().getName());
                Thread.sleep(10 * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

ShutdownHook是实现程序优雅退出的关键,提供了一种方让开发者回收资源、关闭句柄、结束任务等工作。

Java程序优雅退出触发的场景和处理ShutdownHook的过程归纳起来如下图:

在这里插入图片描述


System.exit(int)处理过程

Java虚拟机规范有描述到JVM的退出:

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html#jvms-5.7

5.7. Java Virtual Machine Exit

The Java Virtual Machine exits when some thread invokes the exit method of class Runtime or class System, or the halt method of class Runtime, and the exit or halt operation is permitted by the security manager.

In addition, the JNI (Java Native Interface) Specification describes termination of the Java Virtual Machine when the JNI Invocation API is used to load and unload the Java Virtual Machine.

这段话翻译为中文:

JVM在某个线程调用Runtime类或System类的exit方法,或者调用Runtime类的halt方法,并且这些exit方法或halt方法操作被安全管理器允许时,JVM退出。
此外,JNI(Java Native Interface)规范描述了在使用JNI调用API加载和卸载Java虚拟机时,Java虚拟机的终止情况。

后半段JNI这段话的意思是使用JNI调用本地库的方法时,这个方法里面包含有加载或退出虚拟机的逻辑。

总结一下就是调用以下三个方法之一会使JVM退出:

  • System.exit(int)
  • Runtime.exit(int)
  • Runtime.halt(int)

其中System.exit(int)*调用的是Runtime.exit(int),这两个是同样的效果

public final class System {
    //省略代码...
    public static void exit(int status) {
        Runtime.getRuntime().exit(status);
    }
}

Runtime.exit(int):

public class Runtime {

   /**
     * Terminates the currently running Java virtual machine by initiating its
     * shutdown sequence.  This method never returns normally.  The argument
     * serves as a status code; by convention, a nonzero status code indicates
     * abnormal termination.
     *
     * <p> The virtual machine's shutdown sequence consists of two phases.  In
     * the first phase all registered {@link #addShutdownHook shutdown hooks},
     * if any, are started in some unspecified order and allowed to run
     * concurrently until they finish.  In the second phase all uninvoked
     * finalizers are run if {@link #runFinalizersOnExit finalization-on-exit}
     * has been enabled.  Once this is done the virtual machine {@link #halt
     * halts}.
     *
     * <p> If this method is invoked after the virtual machine has begun its
     * shutdown sequence then if shutdown hooks are being run this method will
     * block indefinitely.  If shutdown hooks have already been run and on-exit
     * finalization has been enabled then this method halts the virtual machine
     * with the given status code if the status is nonzero; otherwise, it
     * blocks indefinitely.
     *
     * <p> The <tt>{@link System#exit(int) System.exit}</tt> method is the
     * conventional and convenient means of invoking this method. <p>
     *
     * @param  status
     *         Termination status.  By convention, a nonzero status code
     *         indicates abnormal termination.
     *
     * @throws SecurityException
     *         If a security manager is present and its <tt>{@link
     *         SecurityManager#checkExit checkExit}</tt> method does not permit
     *         exiting with the specified status
     *
     * @see java.lang.SecurityException
     * @see java.lang.SecurityManager#checkExit(int)
     * @see #addShutdownHook
     * @see #removeShutdownHook
     * @see #runFinalizersOnExit
     * @see #halt(int)
     */
    public void exit(int status) {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkExit(status);
        }
        Shutdown.exit(status);
    }
}

上面exit方法的源码上的注释:

  • 传入参数status为0时,是正常退出
  • 传入参数status不0时,为异常退出

JVM的关闭步骤包含两个步骤:

  1. 运行已经注册的ShutdownHook,它们被无序的执行直到完成。
  2. 如果用setRunFinalizersOnExit设置为true,在关闭之前将会继续调用所有未被调用的 finalizers 方法。
class Shutdown {

    static void exit(int status) {
        boolean runMoreFinalizers = false;
        synchronized (lock) {
            if (status != 0) runFinalizersOnExit = false;
            switch (state) {
            case RUNNING:       /* Initiate shutdown */
                state = HOOKS;
                break;
            case HOOKS:         /* Stall and halt */
                break;
            case FINALIZERS:
                if (status != 0) {
                    /* Halt immediately on nonzero status */
                    halt(status);
                } else {
                    /* Compatibility with old behavior:
                     * Run more finalizers and then halt
                     */
                    runMoreFinalizers = runFinalizersOnExit;
                }
                break;
            }
        }
        if (runMoreFinalizers) {
            runAllFinalizers();
            halt(status);
        }
        synchronized (Shutdown.class) {
            /* Synchronize on the class object, causing any other thread
             * that attempts to initiate shutdown to stall indefinitely
             */
            //开始序列
            sequence();
            //强制终止当前正在运行的Java虚拟机。
            //这个方法接受一个整数参数作为退出状态码,表示程序的退出状态。
            halt(status);
        }
    }
    
    private static void sequence() {
        synchronized (lock) {
            DestroyJavaVM initiates the shutdown sequence
            //防在止DestroyJavaVM开始关闭序列步骤后,另一个线程调用exit造成两次运行
            if (state != HOOKS) return;
        }
        runHooks();
        boolean rfoe;
        synchronized (lock) {
            state = FINALIZERS;
            rfoe = runFinalizersOnExit;
        }
        if (rfoe) runAllFinalizers();
    }
    
    private static void runHooks() {
        for (int i=0; i < MAX_SYSTEM_HOOKS; i++) {
            try {
                Runnable hook;
                synchronized (lock) {
                    // acquire the lock to make sure the hook registered during
                    // shutdown is visible here.
                    currentRunningHook = i;
                    hook = hooks[i];
                }
                if (hook != null) hook.run();
            } catch(Throwable t) {
                if (t instanceof ThreadDeath) {
                    ThreadDeath td = (ThreadDeath)t;
                    throw td;
                }
            }
        }
    }
}    
    
class ApplicationShutdownHooks {
    private static IdentityHashMap<Thread, Thread> hooks;
    static {
        try {
            //
            Shutdown.add(1 /* shutdown hook invocation order */,
                false /* not registered if shutdown in progress */,
                new Runnable() {
                    public void run() {
                        runHooks();
                    }
                }
            );
            hooks = new IdentityHashMap<>();
        } catch (IllegalStateException e) {
            // application shutdown hooks cannot be added if
            // shutdown is in progress.
            hooks = null;
        }
    }
    
    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);
    }

    static void runHooks() {
        Collection<Thread> threads;
        synchronized(ApplicationShutdownHooks.class) {
            threads = hooks.keySet();
            hooks = null;
        }

        for (Thread hook : threads) {
            hook.start();
        }
        for (Thread hook : threads) {
            try {
                hook.join(); //等待钩子线程执行完成
            } catch (InterruptedException x) { }
        }
    }

}

System.exit(int)对于关闭钩子的处理时序如下图:

在这里插入图片描述


非守护线程运行完成退出JVM

Shutdown.shutdown()的JavaDoc提到当最后一个非守护线程完成,本地DestroyJavaVM程序会调用Shutdown.shutdown();与Shutdown.exit(int)不同的是Shutdown.shutdown()不会真正去终止JVM,而是由DestroyJavaVM程序终止。

    /* Invoked by the JNI DestroyJavaVM procedure when the last non-daemon
     * thread has finished.  Unlike the exit method, this method does not
     * actually halt the VM.
     */
    static void shutdown() {
        synchronized (lock) {
            switch (state) {
            case RUNNING:       /* Initiate shutdown */
                state = HOOKS;
                break;
            case HOOKS:         /* Stall and then return */
            case FINALIZERS:
                break;
            }
        }
        synchronized (Shutdown.class) {
            sequence();   //开始序列
        }
    }

来测试一下这个说法,把调试断点放在Shutdown.shutdown()第一行,运行一个最简单的main函数,在main函数这个唯一的非守护线程结束后,断点会运行到Shutdown.shutdown(),验证了这个说法。

在这里插入图片描述

类似的说法在线程类ThreadsetDaemon(..)方法JavaDoc也有提到:当JVM只有守护线程时,JVM会退出。

public class Thread implements Runnable {
    /**
     * Marks this thread as either a {@linkplain #isDaemon daemon} thread
     * or a user thread. The Java Virtual Machine exits when the only
     * threads running are all daemon threads.
     */
    public final void setDaemon(boolean on) {
        checkAccess();
        if (isAlive()) {
            throw new IllegalThreadStateException();
        }
        daemon = on;
    }
    
}

最后一个非守护线程结束后JVM关闭的流程图:

在这里插入图片描述

下面介绍一下用户线程和守护线程

Java线程分为两类:

  • 1、用户线程(非守护线程)
  • 2、守护线程(后台线程)

守护线程是一种特殊的线程,在后台默默地完成一些系统性的服务,比如垃圾回收线程、JIT线程都是守护线程。与之对应的是用户线程,用户线程可以理解为是系统的工作线程,它会完成这个程序需要完成的业务操作。如果用户线程全部结束了,意味着程序需要完成的业务操作已经结束了,系统可以退出了,所以结束所有用户线程的运行,就可以使JVM关闭限出

可以使用jstack -l PID查看线程有没有daemon修饰判断是用户线程还是守护线程。

在这里插入图片描述

来盘点一下让用户线程退出的方法:

1、调用线程的stop()方法(已废弃)

直接退出线程,因为太暴力会产生不可知的结果该方法已废弃。

2、调用线程的interrupt()方法

需要注意的是线程的interrupt()方法不会直接停止线程的运行,需要在interrupt方法后出现的情况在程序自行通过编码结束。当调用线程的interrupt()方法根据以下两种情况出现不同结果:

  • 2.1 当使用interrupt()方法去打断处于阻塞状态的线程时,会抛出InterruptedException异常,而不会更新打断标记,因此,虽然被打断,但是打断标记依然为false。

Thread#isInterrupted()方法可返回打断标记

线程阻塞的情况有以下这些:

 * @see     java.lang.Object#wait()
 * @see     java.lang.Object#wait(long)
 * @see     java.lang.Object#wait(long, int)
 * @see     java.lang.Thread#sleep(long)
 * @see     java.lang.Thread#sleep(long)
 * @see     java.util.concurrent.locks.Condition.await
  • 2.2 当使用interrupt()方法去打断正在运行线程时,被打断的线程会继续运行,但是该线程的打断标记会更新,更新为true,因此可以根据打断标记来作为判断条件使得线程停止。线程是否打断的方法为isInterrupted()

须注意的是调用线程的interrupt()方法并不会停止和关闭线程,程序自行根据打断标记或InterruptedException异常自行结束线程的运行

下面是一个interrupt非守护线程后通过判断线程中断状态结束程序运行的例子:

public class ThreadExitTest {

    public static void main(String[] args) {

        Thread t  = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 100; i++) {

                        System.out.println("Task " + i);

                        if (Thread.currentThread().isInterrupted()) {
                            //如果线程状态为中断,退出循环
                            System.out.println("Thread interrupted! Exiting loop.");
                            return;
                        }
                        Thread.sleep(1000); // 模拟执行任务的耗时
                    }
                } catch (InterruptedException e) {
                    System.out.println("Thread interrupted! Exiting thread.");
                    // 设置线程的中断状态,以确保线程可以正确退出
                    // 如果捕获异常后其它事情可做,也可以直接在此处return
                    Thread.currentThread().interrupt();
                }
            }
        });
        t.setDaemon(false);
        t.start();

        // 让主线程等待一段时间后中断子线程
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.interrupt();
    }

}

再来看看最简单的HelloWorld程序:

public class HelloWorld {

    public static void main(String[] args) {
        System.out.println("hello world!");
    }
}

这是main函数只有一行打印输出控制台的代码,根据上面的理论,就很容易解析为什么HelloWorld程序在打印完hello world!后进程就会退出。

首先当HelloWorld程序启动后,JVM只有一个用户线程main线程,当执行打印的代码后,main线程的任务已经运行完毕,紧接下来的是main线程的结束。当main线程结束后,JVM已经没有用户线程,JVM随之退出。

下面再来看看另一个栗子,起一个子线程,子线程睡眠60秒。

import java.util.concurrent.TimeUnit;

public class JvmExistWhenNonDaemonThreadRunning {

    public static void main(String[] args) throws InterruptedException {
            Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(TimeUnit.SECONDS.toMillis(60));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.setDaemon(false);
        t.start();
        System.out.println("non daemon thread has started...");
    }
}

启动后用以下命令来查询JVM的非守护线程

$ jstack -l 46596 | awk '/tid/ && !/daemon/ {print $0; for(i=1;i<=10;i++) {getline; print}}'

在这里插入图片描述

咦!怎么跟上面讲的不一样,除了子线程"Thread-0"外,还有"DestroyJavaVM"、“VM Thread”、“GC task thread#0 (ParallelGC)”、"VM Periodic Task Thread"等非守护线程。当子线程"Thread-0"运行完毕后,还有这几个非守护线程,这样是不是导致JVM没法退出?

这里引申出另外一个知识点,在main函数结束后,JVM会自动启动一个DestroyJavaVM线程,该线程会等待所用户线程结束后退出(即只剩下daemon 线程、DestroyJavaVM线程自己、VM Thread、VM Periodic Task Thread、GC线程等系统非守护线程,整个虚拟机就退出,此时守护线程被终止)。由此可知这些系统非守护线程并不会影响所有非守护线程结束后JVM的关闭。

系统非守护线程说明
DestroyJavaVM在JVM中所有其他非守护线程全部结束后负责销毁虚拟机。DestroyJavaVM线程在JVM的生命周期中扮演着非常重要的角色,确保资源得到正确的清理和释放。
VM Thread这个线程等待在 JVM 到达安全点进行操作时出现,该线程执行的操作包括“stop the world”的垃圾收集、线程堆栈dump、线程挂起和偏向锁。
VM Periodic Task ThreadVM Periodic Task Thread是JVM中的一个特殊线程,主要负责执行一些周期性的后台任务,包括垃圾回收、性能监控、统计信息收集等。
GC task thread#0 (ParallelGC)并行垃圾回收器, 使用java启动参数-XX:+UseParNewGC时使用这个垃圾回收器。

思考一下问题,上面那个子线程睡眠的例子在运行时正常关闭JVM会出现问题吗

正常关闭的所包含场景可以回看本文第一张配图

答案是会出现问题的。因为JVM退出的所有的清理和关闭钩子都没有对这个睡眠线程作处理,这个线程其实没有得到优雅的退出处理的,最后会让JVM强制关闭退出,线程由此不可控的退出。这种粗暴的退出线程处理在一些对数据保存的场景是不可接受的,比如先将数据保存到数据库,然后更新缓存这两个步骤,如果在第一步保存数据到数据库完成后就线程就被强制退出了,导致数据库和缓存的不一致。

非守护线程的优雅关闭

JVM所有优雅退出的情况都会在退出的时候调用关闭钩子,所以可以用上面介绍到的关闭钩子去实现,以下是一个通过关闭钩子中断任务线程的栗子。每次开始下一次任务时任务线程会根据线程状态是否中断来进行继续下一个任务或结束线程的运行,当JVM退出运行关闭钩子时,中断线程,任务线程的状态设置为中断,任务线程结束运行。

public class JvmThreadElegantExit {

    public static void main(String[] args) {
        //任务线程
        Thread taskThread = new Thread(new Runnable() {
            @Override
            public void run() {

                for (int i = 0; i < 1000; i++) {
                    try {
                        if (Thread.currentThread().isInterrupted()) {
                            System.out.println("task thread is interrupt, exit now...");
                            break;
                        }
                        System.out.println("task " + i + "has done..."); //模拟一次任务处理
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt(); //将线程状态设为中断
                    }
                }

            }
        });
        taskThread.start();

        //关闭钩子
        Thread shutdownHook = new Thread(new Runnable() {
            @Override
            public void run() {
                //中断任务线程
                taskThread.interrupt();
            }
        });
        Runtime.getRuntime().addShutdownHook(shutdownHook);

        //让主线程等待一段时间后关闭JVM
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.exit(0);  //这行也可去掉,改为用kill -15 PID退出JVM
    }
}

线程池ThreadPoolExecutor的关闭

线程池的工作线程默认为非守护线程,其中的核心线程(corePoolSize)空闲时默认不会关闭退出,提交一个任务创建工作线程后不对线程池作操作的话,工作线程会一直保持存活,我们的预期是工作完成后JVM自动退出的,但实际情况是和预期不一致。

这个是一个线程池工作完成后JVM进程一直存活不会退出的栗子:

public class ThreadPoolExecutorKeepAliveExample {

    public static void main(String[] args) throws InterruptedException {

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10, 10, TimeUnit.SECONDS, new LinkedBlockingDeque());

        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                 try {
                    for (int i = 0; i < 10; i++) {
                        System.out.println("sub job " + i + " had done");
                        Thread.sleep(1000);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("all job had done...");
            }
        }););
    }

}

上面是一个线程池里循环执行N个子任务的栗子,在程序启动后对JVM发送SIGTERM信号可以使工作线程关闭,但在Shutdown Hook线程运行完毕后就强制关闭JVM,没有给线程池的工作线程优雅关闭的时机,工作线程在工作中时被强制关闭可能导致任务执行不完整。

如何在JVM退出的时候优雅的关闭?

可以在Shutdown Hook里调用线程池的shutdown()方法并使用awaitTermination(..)等待工作线程完成工作。

public class ThreadPoolExecutorElegantShutdown {

    public static void main(String[] args) throws InterruptedException {

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(50));

        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("job begin...");
                try {
                    for (int i = 0; i < 10; i++) {
                        System.out.println("job processing " + ((i + 1) * 10) + "%");
                        Thread.sleep(2000);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("job had done...");
            }
        });

        //关闭钩子线程
        Thread hookThread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("shutdown hook begin...");
                //关闭线程池
                threadPoolExecutor.shutdown();
                try {
                    //等待30秒使工作线程完成
                    threadPoolExecutor.awaitTermination(30, TimeUnit.SECONDS);
                    System.out.println("shutdown hook end...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        //注册关闭钩子
        Runtime.getRuntime().addShutdownHook(hookThread);
    }
}

JVM启动后,可以用以下命令发送SIGTERM信号:

jps -l | grep ThreadPoolExecutorElegantShutdown | awk '{print $1}' | xargs kill

程序控制台的输出:

job begin...
job processing 10%
job processing 20%
job processing 30%
shutdown hook begin...
job processing 40%
job processing 50%
job processing 60%
job processing 70%
job processing 80%
job processing 90%
job processing 100%
job had done...
shutdown hook end...

可以看出JVM进程在kill命令后工作线程的任务还是继续工作直至完成。

这里用threadPoolExecutor.awaitTermination(30, TimeUnit.SECONDS)使Shutdown Hook线程等待30秒,在上面Systen.exit(int)方法介绍到DestroyVM线程在Shutdown Hook线程启动后会调用Shutdown Hook线程的join()方法等待Shutdown Hook线程完成,如果这里只等待5秒而线程池工作线程还没有结束的话,Shutdown Hook线程结束后DestroyVM就会关闭JVM,也会导致线程池工作线程中断,换句话来说就是只会等待timeout时间让工作线程完成工作。要解决在等待timeout时间后工作线程还没结束的问题,可以把等待的timeout时间设置更长一点,如果线程池工作线程结束的快,不会多浪费时间等待在设定的timeout,在线程池所有工作线程完成后线程池状态变为TERMINATED便会唤醒等待。

计划线程池ScheduledThreadPoolExecutor的关闭

计划线程池ScheduledThreadPoolExecutor和ThreadPoolExecutor一样使用shutdown()关闭,但区别就是计划线程池可以根据业务需要设置参数决定shutdown()后是否要继续运行任务:

  • executeExistingDelayedTasksAfterShutdown:是否在shutdown后继续运行延迟任务
  • continueExistingPeriodicTasksAfterShutdown:是否在shutdown后继续运行周期性任务

参考:

https://juejin.cn/post/7274046488752586811?from=search-suggest

https://stackoverflow.com/questions/32315589/what-happens-when-the-jvm-is-terminated

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1534069.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

揭秘物联网网关,如何工作?功能及选择网关的主要考虑因素

【前言】本篇为物联网硬件系列学习笔记&#xff0c;分享学习&#xff0c;欢迎评论区交流~ 在物联网时代&#xff0c;物联网网关至关重要。它充当传统通信网络和传感网络之间的桥梁。物联网网关作为M2M网关&#xff0c;可以实现各类感知网络之间、感知网络与通信网络之间的协议转…

粤嵌6818开发板如何理解Linux文件IO?

一、文件IO的概述 1、什么是文件&#xff1f; Linux下一切皆文件。普通文件、目录文件、管道文件、套接字文件、链接文件、字符设备文件、块设备文件。 2、什么是IO&#xff1f; input output&#xff1a;输入输出 3、什么是文件IO&#xff1f; 对文件的输入输出&#xff0c;把…

MySQL索引的创建与基本用法

MySQL索引 MySQL索引是一种数据结构&#xff0c;用于提高查询数据的效率。MySQL索引可以被看作是数据库表的“目录”。就像书籍的目录帮助我们快速找到特定章节的位置一样&#xff0c;数据库索引帮助数据库快速找到特定数据记录的位置。 MySQL索引的类型与创建方法 MySQL索引…

TR1 - Transformer起源与发展

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 | 接辅导、项目定制 1. Transformer的起源与发展 2017年Google在《Attention Is All You Need》中提出了Transformer结构用于序列标注&#xff0c;在翻译任务…

最细节操作 Linux LVM 逻辑卷管理

Linux LVM&#xff08;逻辑卷管理&#xff09; 周末愉快&#xff0c;今天带大家实战一下LVM! 一、LVM理论 LVM&#xff0c;即Logical Volume Manager&#xff0c;逻辑卷管理器&#xff0c;是一种硬盘的虚拟化技术&#xff0c;可以允许用户的硬盘资源进行灵活的调整和动态管理…

2023年五级区划省市县乡镇行政村社区边界数据

行政区划数据是重要的基础地理信息数据&#xff0c;根据国家统计局公布的数据&#xff0c;行政区划共分为五级&#xff0c;分别为省级、地级、县级、乡镇/街道级、村/社区级。 该套数据以2020-2023年国家基础地理信息数据中的县区划数据作为矢量基础&#xff0c;辅以高德行政区…

JavaSE:数据类型与变量

目录 一、前言 二、数据类型与变量 &#xff08;一&#xff09;字面常量 &#xff08;二&#xff09;数据类型 &#xff08;三&#xff09;变量 1.变量概念 2.语法格式 3.整型变量 3.1整型变量 3.2长整型变量 3.3短整型变量 3.4字节型变量 4.浮点型变量 4.1双精…

详解main函数参数argc、argv及如何传参

目录 1、main()函数参数 2、main函数如何传参 2.1 环境准备 2.2 通过 Powershell 窗口传参 2.3 通过vs界面传参 3、int main() 和 int main(int argc, char *argv[]) 特点 1、main()函数参数 在C语言中&#xff0c;main函数可以带参数。main函数的原型通常为以下两种形式…

第4章 数据架构

思维导图 架构是构建一个系统&#xff08;如可居住型建筑&#xff09;的艺术和科学&#xff0c;以及在此过程中形成的成果——系统本身。用通俗的话说&#xff0c;架构是对组件要素有组织的设计&#xff0c;旨在优化整个结构或系统的功能、性能、可行性、成本和用户体验。 将架…

Vue cli创建项目时键盘操作无效;vue3.0项目搭建自定义配置

一. 问题描述 在创建vue3.0项目时&#xff0c;在建好的文件夹&#xff0c;鼠标右键 git bash 使用 vue create my-vue3.0创建新项目时&#xff0c;键盘方向键失效&#xff0c;无法选中对应的选项&#xff08;交互提示符不工作&#xff09; 解决方案&#xff1a; 方案一 使用…

章文嵩等技术大咖共同探讨企业数据治理和降本增效策略运用!

3 月 16 日&#xff0c;AutoMQ 携手 OceanBase 开源社区、KubeBlocks 举行的《LLMs 时代下企业数据管理与降本增效之路》主题 meetup 顺利落幕。活动邀请了 AutoMQ 联合创始人 & CSO、Linux LVS 创始人 章文嵩&#xff0c;AutoMQ 联合创始人 & CTO、Apache RocketMQ 联…

一文秒懂什么是客服知识库

大家有没有遇到过这样的情况&#xff1a;打电话给客服&#xff0c;结果对方半天没明白你的问题&#xff0c;或者回答得牛头不对马嘴&#xff1f;这种时候&#xff0c;你是不是觉得特别郁闷&#xff0c;感觉自己的问题就像被丢进了黑洞&#xff0c;永远找不到答案&#xff1f;其…

利用pyvista库可视化点云

ShapeNet分割数据可视化对比 import os import glob import randomimport pyvista as pvresult_paths glob.glob(r./examples/shapenet/results/predict_err_ply/*/*) print(len(result_paths))case_id random.randint(0, len(result_paths) // 3) point_size 3 opacity 0.…

NX二次开发——选择对象控件(清空选择对象)

一、概述 选择对象控件在NX二次开发中经常使用&#xff0c;最近进行学习时发现一片博客中有清空选择对象控件中出现问题&#xff0c;我尝试着写了一下&#xff0c;应该可以解决博主中的问题&#xff0c;其实博主已经写的很详细了&#xff0c;几乎没怎么改&#xff0c;不知道是不…

又一个城市火了,媒介盒子盘点城市爆火原因

近日&#xff0c;“甘肃天水麻辣烫”在各大平台频频登上热搜榜&#xff0c;甘肃当地也及时接住了这泼天富贵&#xff0c;开通“麻辣烫专线”、机场高铁免费接、免费送门票等。这些措施似曾相识&#xff0c;因为在天水前&#xff0c;已经有淄博和哈尔滨这两个城市的案例可以供天…

Linux本地部署TeslaMate结合内网穿透实现公网访问内网车辆信息

文章目录 1. Docker部署TeslaMate2. 本地访问TeslaMate3. Linux安装Cpolar4. 配置TeslaMate公网地址5. 远程访问TeslaMate6. 固定TeslaMate公网地址7. 固定地址访问TeslaMate TeslaMate是一个开源软件&#xff0c;可以通过连接特斯拉账号&#xff0c;记录行驶历史&#xff0c;统…

【C++】输入流操作符 >> 的重载实现

scmd >> icmd 这一行代码的工作原理基于C的标准库对输入流操作符&#xff08;istream extraction operator&#xff0c;即 >>&#xff09;的重载实现。以下是详细的执行顺序和内部逻辑&#xff1a; 对象准备&#xff1a;在此场景中&#xff0c;scmd 是一个 istring…

成都数字产业园有哪些

成都&#xff0c;这座古老而又充满活力的城市&#xff0c;正逐渐成为数字经济的热土。在这座城市的各个角落&#xff0c;数字产业园如雨后春笋般崭露头角&#xff0c;它们为成都的数字化转型注入了强大的动力。首当其冲的便是国际数字影像产业园。 国际数字影像产业园区&#x…

AI写作如何使用,这些方法简单好学!

随着人工智能技术的不断发展&#xff0c;AI写作成为了一个热门话题。许多人都想知道如何使用AI写作来提高效率和质量。在本文中&#xff0c;我们将探讨一些简单而又易学的方法&#xff0c;帮助大家充分利用AI写作。 首先&#xff0c;我们需要了解什么是AI写作。AI写作是指利用人…

测试开发面试题总结(全)

&#x1f345; 视频学习&#xff1a;文末有免费的配套视频可观看 &#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 某基金管理公司线下测试开发面试题总结。 测开题目如下 可以尝试自己先写&#xff0c;写完之后…