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

前言

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

AI:149-法律电子邮件图像中的欺诈检测与敲诈勒索追踪—深度学习技术

🚀点击这里跳转到本专栏,可查阅专栏顶置最新的指南宝典~ 🎉🎊🎉 你的技术旅程将在这里启航! 从基础到实践,深入学习。无论你是初学者还是经验丰富的老手,对于本专栏案例和项目实践都有参考学习意义。 ✨✨✨ 每一个案例都附带关键代码,详细讲解供大家学习,希望…

JSONP漏洞详解

目录 同源策略 JSONP简介 JSONP劫持漏洞 漏洞原理 漏洞利用过程 利用工具 JSONP漏洞挖掘思路 JSONP防御 首先&#xff0c;要了解一下什么是同源策略&#xff1f; 同源策略 同源策略&#xff08;SOP&#xff09;是浏览器的一个安全基石&#xff0c;浏览器为了保证数据…

AI系统性学习01- Prompt Engineering

文章目录 面向开发者的Prompt Engineering一、简介二、Prompt设计原则1 环境配置2.两个基本原则2.1 原则1&#xff1a;编写清晰、具体的指令2.1.1 策略一&#xff1a;分割2.1.2 策略2&#xff1a;结构化输出2.1.3 策略3&#xff1a;模型检测2.1.4 策略4&#xff1a;提供示例 2.…

[数据集][目标检测]焊接件表面缺陷检测数据集VOC+YOLO格式2292张10类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;2292 标注数量(xml文件个数)&#xff1a;2292 标注数量(txt文件个数)&#xff1a;2292 标注…

【GPT-SOVITS-03】SOVITS 模块-生成模型解析

说明&#xff1a;该系列文章从本人知乎账号迁入&#xff0c;主要原因是知乎图片附件过于模糊。 知乎专栏地址&#xff1a; 语音生成专栏 系列文章地址&#xff1a; 【GPT-SOVITS-01】源码梳理 【GPT-SOVITS-02】GPT模块解析 【GPT-SOVITS-03】SOVITS 模块-生成模型解析 【G…

【PyTorch】进阶学习:一文详细介绍 torch.load() 的应用场景、实战代码示例

【PyTorch】进阶学习&#xff1a;一文详细介绍 torch.load() 的应用场景、实战代码示例 &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质量合集】、PyTorch零基础入门教程…

栈和队列(Java实现)

栈和队列&#xff08;Java实现&#xff09; 栈 栈(Stack)&#xff1a;栈是先进后出&#xff08;FILO, First In Last Out&#xff09;的数据结构。Java中实现栈有以下两种方式&#xff1a; stack类LinkedList实现&#xff08;继承了Deque接口&#xff09; &#xff08;1&am…

Python基础算法解析:支持向量机(SVM)

支持向量机&#xff08;Support Vector Machine&#xff0c;SVM&#xff09;是一种用于分类和回归分析的机器学习算法&#xff0c;它通过在特征空间中找到一个最优的超平面来进行分类。本文将详细介绍支持向量机的原理、实现步骤以及如何使用Python进行编程实践。 什么是支持向…

【Java刷题篇】串联所有单词的子串

这里写目录标题 &#x1f4c3;1.题目&#x1f4dc;2.分析题目&#x1f4dc;3.算法原理&#x1f9e0;4.思路叙述✍1.进窗口✍2.判断有效个数✍3.维护窗口✍4.出窗口 &#x1f4a5;5.完整代码 &#x1f4c3;1.题目 力扣链接: 串联所有单词的子串 &#x1f4dc;2.分析题目 阅…

2.vscode 配置python开发环境

vscode用着习惯了,也不想再装别的ide 1.安装vscode 这一步默认已完成 2.安装插件 搜索插件安装 3.选择调试器 Ctrl Shift P&#xff08;或F1&#xff09;&#xff0c;在打开的输入框中输入 Python: Select Interpreter 搜索&#xff0c;选择 Python 解析器 选择自己安…

vulhub中GitLab 远程命令执行漏洞复现(CVE-2021-22205)

GitLab是一款Ruby开发的Git项目管理平台。在11.9以后的GitLab中&#xff0c;因为使用了图片处理工具ExifTool而受到漏洞CVE-2021-22204的影响&#xff0c;攻击者可以通过一个未授权的接口上传一张恶意构造的图片&#xff0c;进而在GitLab服务器上执行任意命令。 环境启动后&am…

深度学习1650ti在win10安装pytorch复盘

深度学习1650ti在win10安装pytorch复盘 前言1. 安装anaconda2. 检查更新显卡驱动3. 根据pytorch选择CUDA版本4. 安装CUDA5. 安装cuDNN6. conda安装pytorch结语 前言 建议有条件的&#xff0c;可以在安装过程中&#xff0c;开启梯子。例如cuDNN安装时登录 or 注册&#xff0c;会…

安卓国产百度网盘与国外云盘软件onedrive对比

我更愿意使用国外软件公司的产品&#xff0c;而不是使用国内百度等制作的流氓软件。使用这些国产软件让我不放心&#xff0c;他们占用我的设备大量空间&#xff0c;在我的设备上推送运行各种无用的垃圾功能。瞒着我&#xff0c;做一些我不知道的事情。 百度网盘安装包大小&…

鸿蒙Next 支持数据双向绑定的组件:Checkbox--Search--TextInput

Checkbox $$语法&#xff0c;$$绑定的变量发生变化时&#xff0c;会触发UI的刷新 Entry Component struct MvvmCase { State isMarry:boolean falseStatesearchText:string build() {Grid(){GridItem(){Column(){Text("checkbox 的双向绑定")Checkbox().select($$…

【PyTorch】基础学习:一文详细介绍 torch.save() 的用法和应用

【PyTorch】基础学习&#xff1a;一文详细介绍 torch.save() 的用法和应用 &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质量合集】、PyTorch零基础入门教程&#x1f44…

ioDraw:与 GitHub、gitee、gitlab、OneDrive 无缝对接,绘图文件永不丢失!

&#x1f31f; 绘图神器 ioDraw 重磅更新&#xff0c;文件保存再无忧&#xff01;&#x1f389; 无需注册&#xff0c;即刻畅绘&#xff01;✨ ioDraw 让你告别繁琐注册&#xff0c;尽情挥洒灵感&#xff01; 新增文件在线实时保存功能&#xff0c;支持将绘图文件保存到 GitHu…

【HarmonyOS】ArkUI - 向左/向右滑动删除

核心知识点&#xff1a;List容器 -> ListItem -> swipeAction 先看效果图&#xff1a; 代码实现&#xff1a; // 任务类 class Task {static id: number 1// 任务名称name: string 任务${Task.id}// 任务状态finished: boolean false }// 统一的卡片样式 Styles func…

机电公司管理小程序|基于微信小程序的机电公司管理小程序设计与实现(源码+数据库+文档)

机电公司管理小程序目录 目录 基于微信小程序的机电公司管理小程序设计与实现 一、前言 二、系统设计 三、系统功能设计 1、机电设备管理 2、机电零件管理 3、公告管理 4、公告类型管理 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八…

【LabVIEW FPGA入门】定时

在本节学习使用循环计时器来设置FPGA循环速率&#xff0c;等待来添加事件之间的延迟&#xff0c;以及Tick Count来对FPGA代码进行基准测试。 1.定时快捷VI函数 在FPGA VI中放置的每个VI或函数都需要一定的时间来执行。您可以允许操作以数据流确定的速率发生&#xff0c;而无需额…

FFmpeg分析视频信息输出到指定格式(csv/flat/ini/json/xml)文件中

1.查看ffprobe帮助 输出格式参数说明: 本例将演示输出csv,flat,ini,json,xml格式 输出所使用的参数如下: 1.输出csv格式: ffprobe -i 4K.mp4 -select_streams v -show_frames -of csv -o 4K.csv 输出: 2.输出flat格式: ffprobe -i 4K.mp4 -select_streams v -show_frames …