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

前言

Java程序启动从main函数开始启动,是程序入口和主线程,但程序会在什么时候结束?为什么有的Java程序在启动后很快就结束了,比如HelloWorld程序,有的程序却能一直在运行,比如Tomcat启动后就一直保持进程不关闭。使用kill 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(int)

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

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

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

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

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

ShutdownHook

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

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

以下是一个简单的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);  //JVM退出,会启动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 ExitThe 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.exit(int)System.exit(int)或者Runtime.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修饰判断是用户线程还是守护线程。

在这里插入图片描述

Tomcat 为什么在启动后JVM进程没有马上结束?

Tomcat8在main函数上使用Socket等待接收命令,因此没有主线程马上结束,保持存活,Tomcat进程随之保持存在。

public final class Bootstrap {private static volatile Bootstrap daemon = null;public static void main(String args[]) {if (daemon == null) {Bootstrap bootstrap = new Bootstrap();//省略代码...daemon = bootstrap;}try {String command = "start";if (command.equals("startd")) {//省略代码...} else if (command.equals("start")) {daemon.setAwait(true);daemon.load(args);//1、启动daemon.start();//省略代码...}} catch (Throwable t) {//省略代码...}}public void start() throws Exception {//2、这里的method是org.apache.catalina.startup.Catalina.start()method.invoke(catalinaDaemon, (Object [])null);}}public class Catalina {public void start() {if (await) {//3、主线程等待await();stop();}}public void await() {//4、服务等待getServer().await();}
}public final class StandardServer extends LifecycleMBeanBase implements Server {public void await() {//省略代码...try {awaitSocket = new ServerSocket(port, 1,InetAddress.getByName(address));} catch (IOException e) {return;}try {awaitThread = Thread.currentThread();// Loop waiting for a connection and a valid commandwhile (!stopAwait) {ServerSocket serverSocket = awaitSocket;if (serverSocket == null) {break;}// Wait for the next connectionSocket socket = null;StringBuilder command = new StringBuilder();try {InputStream stream;long acceptStartTime = System.currentTimeMillis();try {//5、等待接收命令socket = serverSocket.accept();socket.setSoTimeout(10 * 1000);  // Ten secondsstream = socket.getInputStream();} catch (SocketTimeoutException ste) {//省略代码...}//省略代码...} finally {//省略代码...}}} finally {//省略代码...}}
}

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

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() {@Overridepublic 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.");// 设置线程的中断状态,以确保线程可以正确退出// 如果捕获异常后其它事情可做,也可以直接在此处returnThread.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() {@Overridepublic 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,所以可以用上面介绍到的关闭钩子去实现,以下是一个通过关闭钩子中断任务线程的栗子。任务线程每次开始下一次任务时会根据线程状态是否中断来进行继续下一个任务或结束线程的运行,当JVM开始退出并运行关闭钩子时,关闭钩子线程去中断任务线程,任务线程的状态设置为中断,任务线程完成循环体的这次任务后,因为判断线程状态为中断,跳出循环,结束运行,这些行为都是预期的,从而实现该线程的优雅关闭。

public class JvmThreadElegantExit {public static void main(String[] args) {//任务线程Thread taskThread = new Thread(new Runnable() {@Overridepublic 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() {@Overridepublic void run() {//中断任务线程taskThread.interrupt();//等待任务线程结束try {taskThread.join();} catch (InterruptedException e) {e.printStackTrace();}}});Runtime.getRuntime().addShutdownHook(shutdownHook);//让主线程等待一段时间后关闭JVMtry {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() {@Overridepublic 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() {@Overridepublic 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() {@Overridepublic 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命令后工作线程的任务还是继续工作直至完成。

这里Shutdown Hook线程调用threadPoolExecutor.awaitTermination(30, TimeUnit.SECONDS)等待30秒,在上面Systen.exit(int)方法介绍到DestroyVM线程在Shutdown Hook线程启动后会调用Shutdown Hook线程的join()方法等待Shutdown Hook线程完成,如果这里只设置了等待5秒的作为timeout时间而线程池工作线程还没有结束的话,Shutdown Hook线程在等待了5秒后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.rhkb.cn/news/284432.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Excel数字乱码怎么回事 Excel数字乱码怎么调回来

在日常工作中&#xff0c;Excel是我们最常使用的数据处理软件之一&#xff0c;它强大的功能使得数据处理变得既简单又高效。然而&#xff0c;用户在使用Excel时偶尔会遇到数字显示为乱码的问题&#xff0c;这不仅影响了数据的阅读&#xff0c;也大大降低了工作效率。那么&#…

RIPGeo代码理解(六)main.py(运行模型进行训练和测试)

​代码链接:RIPGeo代码实现 ├── preprocess.py # 预处理数据集并为模型运行执行IP聚类 ├── main.py # 运行模型进行训练和测试 ├── test.py #加载检查点,然后测试 一、导入各种模块和数据库 import torch.nnfrom lib.utils import * import argparse i…

数学算法(算法竞赛、蓝桥杯)--最大公约数,欧几里得算法

1、B站视频链接&#xff1a;G05 最大公约数 欧几里得算法_哔哩哔哩_bilibili 题目链接&#xff1a;[NOIP2001 普及组] 最大公约数和最小公倍数问题 - 洛谷 #include <bits/stdc.h> using namespace std; typedef long long LL; LL x,y,ans;LL gcd(LL a,LL b){return b0?…

MongoDB知识

1、部署MongoDB &#xff08;1&#xff09;new好一个mongo文件之后执行 &#xff08;出现mongodb.key&#xff09;记得放行端口 openssl rand -base64 666 > mongodb.key &#xff08;2&#xff09;放到一个docker-compose.yml之后docker-compose up -d执行 version: 3.…

Go——指针和内存逃逸

区别于C/C中的指针&#xff0c;Go语言中的指针不能进行偏移和运算&#xff0c;是安全指针。 要搞明白Go语言中的指针概念需要先知道3个概念&#xff1a;指针地址&#xff0c;指针类型和指针取值。 一. Go语言的指针 Go语言中的函数传参都是值拷贝&#xff0c;当我们想修改某个…

网页无插件视频播放器,支持录像、截图、音视频播放,多路播放等,提供源码下载

前言 本播放器内部采用jessibuca插件接口&#xff0c;支持录像、截图、音视频播放等功能。播放器播放基于ws流&#xff0c;分屏操作支持1分屏、4分屏、6分屏、9分屏方式。 jessibuca工作原理是通过Emscripten将音视频解码库编译成Js&#xff08;WebAssembly&#xff0c;简称was…

【国家计算机二级考试C语言.2024】学习备忘录

说明 分值 4060100 40分&#xff1a; 这里面有一大堆程序结果选这题&#xff0c;如果手速还可以。那遇到有疑问的情况就自己去倒计算器的ad E上面去打一打。能够跑出来&#xff0c;结果那是100%的没问题。 有些概念题比较讨厌&#xff0c;只能自己去记忆了。要去背诵熟熟的。…

AT32F423+DM9051 SPI以太网的MQTT例程说明

DM9051 通过 MQTT 连接阿里云平台 1. 环境简介 作业平台&#xff1a;Windows 硬体环境&#xff1a;AT-START-F423 (AT32F423) 开发板 DM9051 开发板 开发工具&#xff1a;Keil uVision 5 调试工具&#xff1a;Wireshark、串口调试助手 连接平台&#xff1a;阿里云_华东 2 (htt…

海尔洗空气型号KFR-72LW/09HAA81U1技术分析

海尔洗空气型号为:KFR-72LW/09HAA81U1 室外机包含阀门地脚的安装尺寸是960宽703高400深mm 室外机铭牌参数: 制冷运行时 制冷量:7200W(1100-9150) 最大输入功率:3550W 最大电流:16.2A 制热运行时 制热量:9800W(1100-12500) 最大输入功率:6180W 最大电流:28.1A 吸气侧允…

Apache James数据库存储用户信息的密码加密问题

项目场景 Apache James邮件服务器使用数据库来存储用户信息的密码加密问题&#xff1a; 将James的用户改为数据库存储James密码是如何加密验证的 1.将James的用户改为数据库存储 1、修改存储方式 找到james-2.3.2\apps\james\SAR-INF\config.xml 找到<users-store>标…

生产力工具|安装更新R软件(R、studio)

内容介绍&#xff1a; 安装R软件&#xff1a; 下载 R X64 3.5.1: 访问官方R网站 https://cran.r-project.org/。选择适合Windows版本的安装包。将安装包下载到您的计算机。 本地安装: 运行下载的“R-3.5.1-win.exe”文件。按照安装向导&#xff0c;选择安装路径&#xff0c;取消…

墨菲安全在软件供应链安全领域阶段性总结及思考

向外看&#xff1a;墨菲安全在软件供应链安全领域的一些洞察、思考、行动 洞察 现状&挑战&#xff1a; 过去开发安全体系是无法解决软件供应链安全问题的&#xff1b;一些过去专注开发安全领域的厂商正在错误的引导行业用开发安全思维解决软件供应链安全问题&#xff0c;治…

人事管理系统设计与实现|jsp+ Mysql+Java+ B/S结构(可运行源码+数据库+设计文档)

本项目包含可运行源码数据库LW调试部署环境&#xff0c;文末可获取本项目的所有资料。 推荐阅读100套最新项目 最新ssmjava项目文档视频演示可运行源码分享 最新jspjava项目文档视频演示可运行源码分享 最新Spring Boot项目文档视频演示可运行源码分享 2024年56套包含java…

双指针算法:三数之和

文章目录 一、[题目链接&#xff1a;三数之和](https://leetcode.cn/problems/3sum/submissions/515727749/)二、思路讲解三、代码演示 先赞后看&#xff0c;养成习惯&#xff01;&#xff01;&#xff01;^ _ ^<3 ❤️ ❤️ ❤️ 码字不易&#xff0c;大家的支持就是我坚持…

jmeter使用方法---自动化测试

HTTP信息头管理器 一个http请求会发送请求到服务器&#xff0c;请求里面包含&#xff1a;请求头、请求正文、请求体&#xff0c;请求头就是信息头Authorization头的主要用作http协议的认证。 Authorization的作用是当客户端访问受口令保护时&#xff0c;服务器端会发送401状态…

【RPG Maker MV 仿新仙剑 战斗场景UI (八)】

RPG Maker MV 仿新仙剑 战斗场景UI 八 状态及装备场景代码效果 状态及装备场景 本计划在战斗场景中直接制作的&#xff0c;但考虑到在战斗场景中加入太多的窗口这不太合适&#xff0c;操作也繁琐&#xff0c;因此直接使用其他场景。 代码 Pal_Window_EquipStatus.prototype.…

TXT文件内容轻松整理,一键删除文本里的多个内容,轻松整理文档!

在数字化时代&#xff0c;TXT文件已成为我们日常工作和学习的常见文档格式。然而&#xff0c;随着时间的推移&#xff0c;这些文档中可能会积累大量的冗余内容&#xff0c;如重复的文字、无用的注释或不必要的空格。手动删除这些内容不仅费时费力&#xff0c;还可能遗漏或误删重…

【高并发服务器 01】—— 基础知识回顾

接下来四周时间&#xff0c;我将会做一个高并发服务器相关的项目。 前置知识&#xff1a;操作系统系统编程、网络编程、基础的数据结构、C语言。 开发环境&#xff1a;VMware虚拟机&#xff1a;Ubuntu 20.04.6 LTS、vscode 今天先回顾一些基础知识。 1.文件与IO 标准IO&#…

uni-app从零开始快速入门

教程介绍 跨端框架uni-app作为新起之秀&#xff0c;在不到两年的时间内&#xff0c;迅速被广大开发者青睐和推崇&#xff0c;得益于它颠覆性的优势“快”&#xff0c;快到可以节省7套代码。本课程由uni-app开发者团队成员亲授&#xff0c;带领大家无障碍快速掌握完整的uni-app…

pandas的综合练习

事先说明&#xff1a; 由于每次都要导入库和处理中文乱码问题&#xff0c;我都是在最前面先写好&#xff0c;后面的代码就不在写了。要是copy到自己本地的话&#xff0c;就要把下面的代码也copy下。 # 准备工作import pandas as pd import numpy as np from matplotlib impor…