封面来源:碧蓝航线 静海惊雷 活动CG
本文涉及的代码:springboot-study/extension-point/src/main/java/indi/mofan/shutdown
参考链接:
本文基于 SpringBoot 3.1.x
1. 不同的线程名
在 SpringBoot 常用拓展点 一文中介绍过,当容器关闭时会触发两个拓展点,分别是 DisposableBean
和 SmartLifecycle
。
在使用这些拓展点时,打印出了执行它们的线程,比如以 DisposableBean
为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Configuration public class MyShutDownConfig { @Bean public MyDisposableBean myDisposableBean () { return new MyDisposableBean (); } static class MyDisposableBean implements DisposableBean { @Override public void destroy () { System.out.printf("[%s]: 执行 Bean 的销毁方法" , Thread.currentThread().getName()); } } }
提供 shutdown()
方法,获取一个 [0, 9]
之间的随机数,当这个随机数是偶数时,使用 context.close()
关闭容器,否则使用 System.exit(0)
退出程序:
1 2 3 4 5 6 7 8 9 10 11 12 public class ShutdownUtil { public static void shutdown (ConfigurableApplicationContext context) { int randomInt = new Random ().nextInt(0 , 10 ); if ((randomInt & 0b1 ) == 0 ) { System.out.printf("[%s]: 即将执行 context.close() 关闭容器\n" , Thread.currentThread().getName()); context.close(); } else { System.out.printf("[%s]: 即将执行 System.exit(0) 退出程序\n" , Thread.currentThread().getName()); System.exit(0 ); } } }
1 2 3 4 5 6 7 8 @SpringBootApplication public class SpringBootShutdownHook { public static void main (String[] args) { ConfigurableApplicationContext context = SpringApplication.run(SpringBootShutdownHook.class); ShutdownUtil.shutdown(context); } }
多次运行程序,可以得到不同的输出,比如:
[main]: 即将执行 context.close() 关闭容器
[main]: 执行 Bean 的销毁方法
再比如:
[main]: 即将执行 System.exit(0) 退出程序
[SpringApplicationShutdownHook]: 执行 Bean 的销毁方法
可以发现:
使用 ConfigurableApplicationContext#close()
关闭容器时,销毁方法在主(main)线程中被执行;
使用 System.exit(0)
退出程序时,销毁方法会在名为 SpringApplicationShutdownHook
的线程中被执行。
为什么会这样呢?问题可能就在 System.exit()
方法。
2. System.exit()
2.1 使用方式
这是 System
类中的一个静态方法:
1 2 3 public static void exit (int status) { Runtime.getRuntime().exit(status); }
System
类还是非常熟悉的,学习 Java 遇到的第一个类就是 System
,因为要用 System.out.println()
打印 Hello World
🤣, 除此之外,还可以使用 System.getProperty()
获取一些环境变量,比如:
1 2 System.getProperty("java.version" )
言归正传,System.exit()
方法有什么用呢?它的方法注释是这么说的:
Terminates the currently running Java Virtual Machine. The
argument serves as a status code; by convention, a nonzero status
code indicates abnormal termination.
This method calls the exit
method in class
Runtime
. This method never returns normally.
The call System.exit(n)
is effectively equivalent to
the call:
Runtime.getRuntime().exit(n)
翻译下:System.exit()
方法是用来终止当前正在运行的 Java 虚拟机。方法参数作为状态码,按照惯例,非零状态码表示异常终止。System.exit()
方法调用了 Runtime
的 exit()
方法。 这个方法从不正常返回。 调用 System.exit(n)
等价于调用 Runtime.getRuntime().exit(n)
。
2.2 从不正常返回
在面试题中经常会看到 try-catch-finally
搭配 return
,并要求算出最终返回的值。比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 private static int tryCatchFinallyQuestion (int param) { try { if (param > 10 ) { throw new RuntimeException (); } System.out.println("try param: " + param); return param; } catch (Exception e) { param++; System.out.println("catch param: " + param); return param; } finally { param++; System.out.println("finally param: " + param); if (param > 20 || param < 0 ) { return param; } } } public static void main (String[] args) { int result = tryCatchFinallyQuestion(2 ); System.out.println("result: " + result); System.out.println("------------------------" ); result = tryCatchFinallyQuestion(-1 ); System.out.println("result: " + result); System.out.println("------------------------" ); result = tryCatchFinallyQuestion(11 ); System.out.println("result: " + result); System.out.println("------------------------" ); result = tryCatchFinallyQuestion(21 ); System.out.println("result: " + result); }
输出是什么?
try param: 2
finally param: 3
result: 2
------------------------
try param: -1
finally param: 0
result: -1
------------------------
catch param: 12
finally param: 13
result: 12
------------------------
catch param: 22
finally param: 23
result: 23
问题不难,主要是想说明就算 return
了,finally
块中的代码也依旧会执行,如果 finally
块中也 return
了,甚至会以 finally
块中的 return
为主。
那么这个方法会输出什么呢?
1 2 3 4 5 6 7 8 9 10 private static void neverReturnsNormally () { try { System.out.println("try block" ); System.exit(0 ); } catch (Exception e) { System.out.println("catch block" ); } finally { System.out.println("finally block" ); } }
finally
块中的代码还会执行吗?
try block
答案是否定的,只执行了 try
块中的代码,这就是 System.exit()
方法从不正常返回的含义。
这其实很好理解,System.exit()
方法会终止正在运行的 JVM,JVM 都被终止了,finally
块中的代码还怎么执行?
2.3 源码剖析
System.exit()
方法其实是调用了 Runtime
类中的 exit()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class Runtime { private static final Runtime currentRuntime = new Runtime (); private static Version version; public static Runtime getRuntime () { return currentRuntime; } public void exit (int status) { @SuppressWarnings("removal") SecurityManager security = System.getSecurityManager(); if (security != null ) { security.checkExit(status); } Shutdown.exit(status); } }
然后又调用了 Shutdown
中的 exit()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 static void exit (int status) { synchronized (lock) { if (status != 0 && VM.isShutdown()) { halt(status); } } synchronized (Shutdown.class) { beforeHalt(); runHooks(); halt(status); } }
主要方法有三个,其中的 beforeHalt()
是一个本地方法,而 halt()
方法最终也是去调用的本地方法,略过它们,重点放在 java.lang.Shutdown#runHooks()
方法上:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 private static void runHooks () { synchronized (lock) { if (VM.isShutdown()) return ; } for (int i=0 ; i < MAX_SYSTEM_HOOKS; i++) { try { Runnable hook; synchronized (lock) { currentRunningHook = i; hook = hooks[i]; } if (hook != null ) hook.run(); } catch (Throwable t) { if (t instanceof ThreadDeath td) { throw td; } } } VM.shutdown(); }
主要逻辑是循环 MAX_SYSTEM_HOOKS
,然后从 hooks
数组中取出 hook
并一一执行。
MAX_SYSTEM_HOOKS
是一个整型常量,数值为 10,难道 Java 最多只能让用户定义 10 个 Shutdown Hook?
1 2 3 4 5 6 7 8 private static final int MAX_SYSTEM_HOOKS = 10 ;private static final Runnable[] hooks = new Runnable [MAX_SYSTEM_HOOKS];
原来这里的 hooks
并不是用户自定义的 Shutdown Hook,而是预定义的系统 Shutdown Hook,在 JDK17 中按以下顺序预定了三个:
Console restore hook
ApplicationShutdownHooks
DeleteOnExit hook
其中的 ApplicationShutdownHooks
才是用户自定义的 Shutdown Hook。
那 ApplicationShutdownHooks
又是怎么被定义的呢?
先看 java.lang.Shutdown#hooks
常量在哪里被赋值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 static void add (int slot, boolean registerShutdownInProgress, Runnable hook) { if (slot < 0 || slot >= MAX_SYSTEM_HOOKS) { throw new IllegalArgumentException ("Invalid slot: " + slot); } synchronized (lock) { if (hooks[slot] != null ) throw new InternalError ("Shutdown hook at slot " + slot + " already registered" ); if (!registerShutdownInProgress) { if (currentRunningHook >= 0 ) throw new IllegalStateException ("Shutdown in progress" ); } else { if (VM.isShutdown() || slot <= currentRunningHook) throw new IllegalStateException ("Shutdown in progress" ); } hooks[slot] = hook; } }
在 java.lang.Shutdown#add()
方法中存在赋值逻辑,再看 add()
方法会在哪被调用,其中一处是在 ApplicationShutdownHooks
类中,光看这类名就感觉不简单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class ApplicationShutdownHooks { private static IdentityHashMap<Thread, Thread> hooks; static { try { Shutdown.add(1 , false , new Runnable () { public void run () { runHooks(); } } ); hooks = new IdentityHashMap <>(); } catch (IllegalStateException e) { hooks = null ; } } }
ApplicationShutdownHooks
类初始化时会执行 static
块,在这里面注册了 ApplicationShutdownHooks
。其中的 runHooks()
方法负责执行用户所有自定义的 Shutdown Hook:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 static void runHooks () { Collection<Thread> threads; synchronized (ApplicationShutdownHooks.class) { threads = hooks.keySet(); hooks = null ; } for (Thread hook : threads) { hook.start(); } for (Thread hook : threads) { while (true ) { try { hook.join(); break ; } catch (InterruptedException ignored) { } } } }
ApplicationShutdownHooks
类中的 hooks
才是用户自定义的 Shutdown Hook,并且每一个 Hook 对应一个 Thread
对象,在执行时,循环调用它们的 start()
方法开启线程,这说明用户自定义的 Shutdown Hook 是并发无序执行的。
然后又对注册的每个 Hook 进行遍历,采用死循环并调用 join()
方法死等每个 Hook 执行完,也就是说, 如果有某个 Hook 在执行时内部发生死锁或死循环,调用的 System.exit()
方法会一直执行,始终无法正常退出程序。
用户自定义 Shutdown Hook 的执行解决了,那要怎么注册呢?
查看 java.lang.ApplicationShutdownHooks#hooks
变量会在哪被使用,或者说在哪被初始化?
在 java.lang.ApplicationShutdownHooks#add()
方法中调用了 hooks.put()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 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); }
但这个方法并不是外部可见的,它又会在哪被调用呢?
它只会在 java.lang.Runtime#addShutdownHook()
方法中被调用:
1 2 3 4 5 6 7 8 public void addShutdownHook (Thread hook) { @SuppressWarnings("removal") SecurityManager sm = System.getSecurityManager(); if (sm != null ) { sm.checkPermission(new RuntimePermission ("shutdownHooks" )); } ApplicationShutdownHooks.add(hook); }
java.lang.Runtime#addShutdownHook()
是外部可见的,用户可以使用该方法来注册自定义的 Shutdown Hook。
2.4 给局部变量赋值加锁?
System.exit()
的源码中有个细节,它在 java.lang.Shutdown#runHooks()
方法中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 private static void runHooks () { for (int i=0 ; i < MAX_SYSTEM_HOOKS; i++) { try { Runnable hook; synchronized (lock) { currentRunningHook = i; hook = hooks[i]; } } catch (Throwable t) { } } }
hook
是一个局部变量,为它的赋值加个锁是什么意思?局部变量又不会出现线程安全问题。
这里使用了 Happens-Before
原则,通常认为 Happens-Before
原则共有 8 条,其中一条名为 Monitor Lock Rule
,其含义是:
An unlock on a monitor happens-before every subsequent lock on that monitor
一个锁的解锁总是发生在后续对这个锁的加锁之前。 比如以 synchronized
关键词为例:
1 2 3 4 5 6 7 8 9 10 11 public class MonitorLockRule { private int x = 0 ; public void changeX () { synchronized (this ) { if (this .x < 10 ) { this .x = 10 ; } } } }
在进入 synchronized
块之前,会自动加锁,代码块执行完毕后,又会自动解锁,加锁与解锁操作由编译器完成。
x
的初始值是 0
,当线程 A 执行完 changeX()
方法,后续线程 B 执行 changeX()
方法时,线程 B 对能够感知到 x
变成了 10
,保证了 x
的内存可见性,这个操作也是符合自觉的。
回到对局部变量 hook
的赋值,单从这里看确实有点迷,此处是对成员变量 hooks
的读操作,结合它的写操作来看看,即 java.lang.Shutdown#add()
方法:
1 2 3 4 5 6 7 8 static void add (int slot, boolean registerShutdownInProgress, Runnable hook) { synchronized (lock) { hooks[slot] = hook; } }
在写操作时,也进行了加锁操作,这样与读操作的加锁操作结合,就能满足 Happens-Before
原则。
如果局部变量 hook
的赋值 没有加锁
假设 hooks
现在前三个位置有值,也就是系统预定义的 Shutdown Hook,记为 A、B、C。
再假设执行到 B 时,在另一个线程中修改 hooks[2]
,将其由 C 修改为 D,整个修改操作比较慢,当 B 都执行完了,准备执行下一个 Shutdown Hook 时,修改操作都还没有完成。
由于局部变量 hook
的赋值没有加锁,此时读取到的下一个 Shutdown Hook 仍然是 C,而不是 D。
如果局部变量 hook
的赋值 加了锁
还是上述的场景,执行 B 没有加锁操作,因此在另一个线程将 C 修改为 D 时能够直接拿到锁,整个操作还是很慢,当 B 执行完了后,准备执行下一个 Shutdown Hook 时,需要先进行局部变量 hook
的赋值,此时需要一个锁,但锁仍被另一个线程持有,等到将 C 成功修改为 D 之后释放了锁,才能进行 hook
的赋值操作。
此时 C 已经被修改成了 D,再去读取下一个 Shutdown Hook 时,读取到的就是 D 而不是 C。
2.5 Java Shutdown Hook
触发时机
触发时机可以分为两种,分别是 程序正常退出 和 程序响应外部事件 :
程序正常退出:
最后一个非守护线程执行完毕时退出
System.exit()
方法被调用时
程序响应外部事件
响应用户输入,比如使用 Ctrl + c
捕获 kill
信号,比如 kill -1
(重新加载进程)、kill -2
(中断进程,与 Ctrl + c
类似)、kill -15
(正常停止一个进程),但熟悉的 kill -9
并不会触发 Shutdown Hook,因为它是直接杀死进程
响应系统事件,比如用户注销、系统关机
存在的意义
Java Shutdown Hook 的存在是为了实现优雅停机,比如在停机时实现资源的关闭、保证业务的连续性。
以保证业务连续性为例,在一个系统中存在生产者和消费者,当该系统被停机时,生产者与消费者依次下线,还需要:
后下线的消费者不能请求到已下线的生产者
对于已经产生的请求,要求消费者处理完毕后才下线(可以设置一个超时时间,防止消费者迟迟不下线)
以上需求都可以通过 Java Shutdown Hook 来完成。
3. SpringBoot 优雅停机
3.1 注册 Shutdown Hook
在执行 SpringApplication#run(java.lang.String...)
方法时,会调用 refreshContext()
方法:
1 2 3 4 5 6 7 8 9 private void refreshContext (ConfigurableApplicationContext context) { if (this .registerShutdownHook) { shutdownHook.registerApplicationContext(context); } refresh(context); }
默认情况下会注册 Shutdown Hook,执行 SpringApplicationShutdownHook#registerApplicationContext()
方法完成 Shutdown Hook 的注册:
1 2 3 4 5 6 7 8 void registerApplicationContext (ConfigurableApplicationContext context) { addRuntimeShutdownHookIfNecessary(); synchronized (SpringApplicationShutdownHook.class) { assertNotInProgress(); context.addApplicationListener(this .contextCloseListener); this .contexts.add(context); } }
重点在其中的 addRuntimeShutdownHookIfNecessary()
方法:
1 2 3 4 5 private void addRuntimeShutdownHookIfNecessary () { if (this .shutdownHookAdditionEnabled && this .shutdownHookAdded.compareAndSet(false , true )) { addRuntimeShutdownHook(); } }
再进入 addRuntimeShutdownHook()
方法:
1 2 3 void addRuntimeShutdownHook () { Runtime.getRuntime().addShutdownHook(new Thread (this , "SpringApplicationShutdownHook" )); }
调用了前文说到的 java.lang.Runtime#addShutdownHook()
方法完成 Shutdown Hook 的注册。
线程名被定义为 SpringApplicationShutdownHook
,与开篇测试时打印的线程名一致。
构造方法的第一个参数传入了 this
,这是因为当前类 SpringApplicationShutdownHook
实现了 Runnable
接口,重写后的 run()
方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Override public void run () { Set<ConfigurableApplicationContext> contexts; Set<ConfigurableApplicationContext> closedContexts; Set<Runnable> actions; synchronized (SpringApplicationShutdownHook.class) { this .inProgress = true ; contexts = new LinkedHashSet <>(this .contexts); closedContexts = new LinkedHashSet <>(this .closedContexts); actions = new LinkedHashSet <>(this .handlers.getActions()); } contexts.forEach(this ::closeAndWait); closedContexts.forEach(this ::closeAndWait); actions.forEach(Runnable::run); }
比较简单,重点在 closeAndWait()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 private void closeAndWait (ConfigurableApplicationContext context) { if (!context.isActive()) { return ; } context.close(); try { int waited = 0 ; while (context.isActive()) { if (waited > TIMEOUT) { throw new TimeoutException (); } Thread.sleep(SLEEP); waited += SLEEP; } } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } catch (TimeoutException ex) { } }
在 closeAndWait()
方法中,还是调用了 close()
方法,这也是为什么使用 context.close()
关闭容器和 System.exit(0)
退出程序执行的逻辑基本一致的原因,它们最终都会调用 context.close()
方法,只不过执行的线程不一样。
3.2 只使用了 Spring
在 SpringBoot 应用中会自动注册 Shutdown Hook,如果只使用了 Spring 呢(虽然现在不大可能了)?
只使用了 Spring 的情况下,不会自动注册 Shutdown Hook,需要手动调用 AbstractApplicationContext#registerShutdownHook()
方法进行注册。比如:
1 2 3 4 5 6 7 8 9 public class SpringShutdownHook { public static void main (String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext (MyShutDownConfig.class); context.registerShutdownHook(); ShutdownUtil.shutdown(context); } }
运行结果与 SpringBoot 应用基本无异。
看看 registerShutdownHook()
方法做了啥:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Override public void registerShutdownHook () { if (this .shutdownHook == null ) { this .shutdownHook = new Thread (SHUTDOWN_HOOK_THREAD_NAME) { @Override public void run () { synchronized (startupShutdownMonitor) { doClose(); } } }; Runtime.getRuntime().addShutdownHook(this .shutdownHook); } }
其实就是实例化了个 Thread
,重写的 run()
方法中调用了 doClose()
方法,最后注册 Shutdown Hook。
至于 doClose()
方法也没啥可分析的,如果调用 context.close()
方法最终也会调用 doClose()
方法,两个都差不多。
关于 doClose()
方法的内部执行逻辑,可以参考 SpringBoot 常用拓展点 一文中对 DisposableBean
和 SmartLifecycle
两个拓展点执行的分析。
哪有什么岁月静好,都是 SpringBoot 在为你负重前行。👻
4. 无法正常退出
通过前文对 System.exit()
方法的源码剖析可知: 用户自定义的 Shutdown Hook 会并发无序执行,并且每执行一个 Shutdown Hook 时,会死等它执行完。
那么这可能会导致尽管调用了 System.exit()
方法,但程序仍然无法正常退出。比如某个 Shutdown Hook 中:
4.1 存在死循环
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class UnableExit { public static void main (String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext (Config.class); context.registerShutdownHook(); simpleExit(); } private static void simpleExit () { System.exit(0 ); } @Configuration static class Config { @Bean public UnableExitApi unableExitApi () { return new EndlessLoop (); } } interface UnableExitApi { } @SuppressWarnings("all") static class EndlessLoop implements UnableExitApi , DisposableBean { @Override public void destroy () throws Exception { while (true ) { System.out.println("I'm still alive" ); TimeUnit.SECONDS.sleep(5 ); } } } }
每隔 5 秒就打印出 I'm still alive
,程序始终无法正常退出。
4.2 存在死锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class UnableExit { public static void main (String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext (Config.class); context.registerShutdownHook(); exitWithLock(); } private static final Lock LOCK = new ReentrantLock (); private static void exitWithLock () { LOCK.lock(); System.exit(0 ); LOCK.unlock(); } @Configuration static class Config { @Bean public UnableExitApi unableExitApi () { return new Deadlock (); } } static class Deadlock implements UnableExitApi , DisposableBean { @Override public void destroy () { LOCK.lock(); System.out.println("Hey! I'm still alive!" ); LOCK.unlock(); System.out.println("Oh, no! I'm dead." ); } } }
控制台一片空白,没有输出任何内容,程序也无法正常退出。
当执行 exitWithLock()
方法时,获取到锁 LOCK
,然后执行 System.exit(0)
触发 Shutdown Hook,在执行 Shutdown Hook 时会执行 Deadlock#destroy()
方法,该方法也需要获取到锁 LOCK
,此时的锁仍被主线程占用,需要 System.exit(0)
方法执行完才能解锁,但它能执行完的前提是能够执行完 destroy()
方法,这就出现:
main:你执行完,我就解锁;
SpringContextShutdownHook:你解锁,我才能执行完;
形成死锁,无法正常退出。
如果进行一点点改动,比如:
1 2 3 4 5 6 private static void exitWithLock () { LOCK.lock(); new Thread (() -> System.exit(0 )).start(); LOCK.unlock(); }
此时再运行程序,控制台打印出:
Hey! I'm still alive!
Oh, no! I'm dead.
程序也能正常退出。
这其实很好理解,退出程序的逻辑不在主线程中,那个线程不持有任何锁,主线程一路向下执行并释放锁,释放锁的瞬间,执行 Shutdown Hook 的线程就能拿到锁,然后执行,最终成功退出程序。
在这个基础上还能再变,比如在 Deadlock#destroy()
方法中又写个死循环,尽管退出程序的逻辑不在主线程中,程序又无法正常退出了。
5. 优雅关闭线程池
5.1 一般情况下
以 JDK 中的 ThreadPoolExecutor 为例,一般情况下关闭线程池涉及到三个方法:
shutdown()
shutdownNow()
awaitTermination()
一个任务一般会有以下几种情况:
未提交到线程池
已提交到线程池,但还未执行
正在执行
执行完毕
shutdown()
执行该方法后:
拒绝新任务的提交,如果还有任务提交,则抛出 RejectedExecutionException
待执行的任务继续执行
正在执行的任务也继续执行
在该方法的注释上有这样一句话:
This method does not wait for previously submitted tasks to
complete execution. Use awaitTermination to do that.
它说:这个方法不会等待以前提交的任务执行完,要使用 awaitTermination()
方法来完成。
不是说待执行的任务还会继续执行吗,这里又说不等待是什么意思?看一个示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public static void main (String[] args) { useShutdownOnly(); } private static ExecutorService getExecutorService () { ExecutorService service = Executors.newFixedThreadPool(1 ); for (int i = 1 ; i <= 5 ; i++) { String str = i + "" ; service.execute(new Runnable () { @Override public void run () { System.out.println(str); } @Override public String toString () { return "第 " + str + " 个任务" ; } }); } return service; } private static void useShutdownOnly () { ExecutorService service = getExecutorService(); service.shutdown(); System.out.println("线程池关闭了" ); }
线程池关闭了
1
2
3
4
5
主线程中的内容最先被输出,之后输出每个任务中的内容。
注释中说的“不会等待以前提交的任务执行完”指的是:这个方法不会阻塞,其他线程可以在这些任务前执行,如果要实现阻塞,可以使用 awaitTermination()
方法。比如:
1 2 3 4 5 6 7 8 @SneakyThrows @SuppressWarnings("ResultOfMethodCallIgnored") private static void shutdownAndAwait () { ExecutorService service = getExecutorService(); service.shutdown(); service.awaitTermination(1 , TimeUnit.MINUTES); System.out.println("线程池关闭了" ); }
1
2
3
4
5
线程池关闭了
主线程的内容在线程池的任务执行完毕后才被输出,主线程被阻塞了。
shutdownNow()
执行该方法后:
拒绝新任务的提交,如果还有任务提交,则抛出 RejectedExecutionException
取消待执行的任务,并将它们作为该方法的返回值
尝试取消正在执行的任务,仅仅是尝试
看一个示例:
1 2 3 4 5 6 private static void shutdownNow () { ExecutorService service = getExecutorService(); List<Runnable> list = service.shutdownNow(); System.out.println("线程池关闭了" ); list.forEach(System.out::println); }
线程池关闭了
1
第 2 个任务
第 3 个任务
第 4 个任务
第 5 个任务
主线程的内容输出后,正在执行的任务才执行完,也可以使用 shutdownAndAwait()
方法阻塞下:
1 2 3 4 5 6 7 8 9 @SneakyThrows @SuppressWarnings("ResultOfMethodCallIgnored") private static void shutdownNowAndAwait () { ExecutorService service = getExecutorService(); List<Runnable> list = service.shutdownNow(); service.awaitTermination(1 , TimeUnit.MINUTES); System.out.println("线程池关闭了" ); list.forEach(System.out::println); }
1
线程池关闭了
第 2 个任务
第 3 个任务
第 4 个任务
第 5 个任务
综上可知,一般情况下关闭线程池可以使用 shutdown() + awaitTermination()
,或者 shutdownNow() + awaitTermination()
。
当然也可以来个三合一,比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ExecutorService executor = Executors.newCachedThreadPool();executor.shutdown(); try { if (!executor.awaitTermination(60 , TimeUnit.SECONDS)) { executor.shutdownNow(); } } catch (InterruptedException e) { executor.shutdownNow(); Thread.currentThread().interrupt(); }
5.2 错误关闭线程池
当执行某个任务依赖容器的某个 Bean 时,但这个 Bean 在线程池关闭期间被先行销毁了,导致剩余任务不能执行完,这就会导致业务的不完整。
比如在实际场景中,线程池中的某个任务去操作了 Redis,当任务还未执行完时,收到了停机的指令,此时触发 Shutdown Hook,但 Redis 连接先一步被回收,导致线程池中的任务获取不到 Redis 连接,进而产生报错。
模拟下这个场景:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 @SpringBootApplication public class ErrorShutdownThreadPool { public static void main (String[] args) { ConfigurableApplicationContext context = SpringApplication.run(ErrorShutdownThreadPool.class); executeManually(context); System.exit(0 ); } private static Object connection = new Object (); @Component static class RedisService implements DisposableBean { public Object getConnection () { if (connection == null ) { throw new RuntimeException ("connection is null" ); } return connection; } @Override public void destroy () { connection = null ; } } private static void executeManually (ApplicationContext context) { RedisService redisService = context.getBean(RedisService.class); ExecutorService service = Executors.newFixedThreadPool(1 ); execute(service, redisService); Runtime.getRuntime().addShutdownHook(new Thread (destroyThreadPool(service))); } private static Runnable destroyThreadPool (ExecutorService service) { return () -> { service.shutdown(); try { service.awaitTermination(1 , TimeUnit.MINUTES); } catch (InterruptedException e) { throw new RuntimeException (e); } }; } private static void execute (Executor service, RedisService redisService) { service.execute(() -> { System.out.println("开始执行任务一" ); System.out.println("获取连接: " + redisService.getConnection()); try { TimeUnit.SECONDS.sleep(3 ); } catch (InterruptedException e) { throw new RuntimeException (e); } System.out.println("任务一执行结束" ); }); service.execute(() -> { System.out.println("开始执行任务二" ); System.out.println("获取连接: " + redisService.getConnection()); try { TimeUnit.SECONDS.sleep(3 ); } catch (InterruptedException e) { throw new RuntimeException (e); } System.out.println("任务二执行结束" ); }); } }
Redis 连接的回收由 SpringBoot Shutdown Hook 完成,而线程池的关闭由另一个 Shutdown Hook 完成,它们并行执行,此时就可能出现问题:
开始执行任务一
获取连接: java.lang.Object@69ede4fc
任务一执行结束
开始执行任务二
Exception in thread "pool-2-thread-1" java.lang.RuntimeException: connection is null
执行任务二获取连接时,连接已被回收,导致抛出异常。
5.3 优雅关闭
将线程池交给 Spring 管理,并且实现 DisposableBean
接口完成线程池的关闭即可。比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Component static class MyThreadPool implements ApplicationRunner , DisposableBean { @Autowired private RedisService redisService; ExecutorService service = Executors.newFixedThreadPool(1 ); @Override public void run (ApplicationArguments args) throws Exception { execute(service, redisService); } @Override public void destroy () { destroyThreadPool(service).run(); } }
此时再次运行,能够使得线程池先关闭再回收连接:
开始执行任务一
获取连接: java.lang.Object@50be9718
任务一执行结束
开始执行任务二
获取连接: java.lang.Object@50be9718
任务二执行结束
原理和 DisposableBean
的执行有关,可以参考 SpringBoot 常用拓展点 一文中对 DisposableBean
执行的讲解。简单来说就是 SpringBoot 在执行销毁方法时,会判断哪些 Bean 依赖了当前 Bean,将那些 Bean 的销毁方法执行完毕后再执行当前 Bean 的销毁方法。
假设准备先执行 RedisService
的销毁方法,发现 MyThreadPool
又依赖了它,进而先执行后者的销毁方法并关闭线程池,之后再回收连接。
为了简化开发,SpringBoot 提供了类似的类,即 ThreadPoolTaskExecutor
和 RedisTemplate
,销毁方法都由 SpringBoot 重写好了,直接使用即可。