线程池导致的 shutdown失败的完整排查过程

SpringBoot 中有一种方式可以优雅地关闭应用程序。

(优雅停机是指​关闭应用程序时,在规定的超时时间范围内,允许进行中的请求完成,拒绝新的请求进入​。 这将使应用在请求处理方面保持一致,即没有未处理请求,每一个请求都被处理(完成或拒绝)

配置如下

yml 复制代码
server:
  port: 8888
  shutdown: graceful
management:
  endpoint:
    shutdown:
      enabled: true  
  endpoints:
    web:
      exposure:
        include: shutdown
xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

现象

直接调用 localhost:8888/actuator/shutdown 即可关闭应用程序。但是在调用某个业务后,再调用 shutdown 的 api,发现实际 shutdown 确实在执行,但是最终并没有把 pid 给 kill 掉,应用程序依然在运行。

第一怀疑就是认为这个程序执行后,还有什么资源没有被关闭掉,导致 springboot 认为应用程序还在运行,从而没有执行关闭操作。

排查过程

执行脚本

shell 复制代码
生成线程快照
jstack -l pid > threads.txt

# 查询非守护进程(因为非守护线程会阻止 JVM 退出) -v 表示反向排除
grep -n '" ' threads2.txt | grep -v daemon
  • 所有线程信息
  • 非守护线程信息

在里面发现了一段 关于 "pool-X-thread-Y"的线程信息,这个 ThreadPoolExecutor 出来的线程,处于等待中,其他的都是额外框架的线程信息或者 jvm 的,只有"pool-X-thread-Y"属于额外的。

less 复制代码
"pool-4-thread-1" #230 prio=5 os_prio=31 cpu=0.21ms elapsed=252.08s tid=0x00000001642cf800 nid=0x9a07 waiting on condition  [0x000000017aaf6000]
   java.lang.Thread.State: WAITING (parking)
	at jdk.internal.misc.Unsafe.park(java.base@17.0.13/Native Method)
	- parking to wait for  <0x0000000701e8af30> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
	at java.util.concurrent.locks.LockSupport.park(java.base@17.0.13/LockSupport.java:341)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionNode.block(java.base@17.0.13/AbstractQueuedSynchronizer.java:506)
	at java.util.concurrent.ForkJoinPool.unmanagedBlock(java.base@17.0.13/ForkJoinPool.java:3465)
	at java.util.concurrent.ForkJoinPool.managedBlock(java.base@17.0.13/ForkJoinPool.java:3436)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(java.base@17.0.13/AbstractQueuedSynchronizer.java:1630)
	at java.util.concurrent.ArrayBlockingQueue.take(java.base@17.0.13/ArrayBlockingQueue.java:420)
	at java.util.concurrent.ThreadPoolExecutor.getTask(java.base@17.0.13/ThreadPoolExecutor.java:1062)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@17.0.13/ThreadPoolExecutor.java:1122)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@17.0.13/ThreadPoolExecutor.java:635)
	at java.lang.Thread.run(java.base@17.0.13/Thread.java:840)

   Locked ownable synchronizers:
	- None
  • ThreadPoolExecutor 默认线程名称源码

有了这个排查方向,去项目里面查找关于ThreadPoolExecutor的代码。

最终发现一句关于线程池的声明代码。从代码来看,虽然 XxxConfig 类上加了 @Configuration 注解,受到 spring 管理,但是 XXX_EXECUTOR 这个线程池是静态变量, 并没有受到 spring 管理,所以 springboot 在执行 shutdown 的时候,并不会关闭这个线程池,导致应用程序没有被关闭。

java 复制代码
@Configuration
public class XxxConfig { 
    public static final ThreadPoolExecutor XXX_EXECUTOR = new ThreadPoolExecutor(20, 20, 1000, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(10000), new ThreadPoolExecutor.CallerRunsPolicy());

}

最终解决方案

建议将 XXX_EXECUTOR 这个线程池改为 spring 管理的 bean,如下所示:

java 复制代码
@Configuration
public class XxxConfig {
    @Bean("xxxExecutor")
    public ThreadPoolExecutor xxxExecutor() {
        //示例 demo
        return new ThreadPoolExecutor(20, 20, 1000, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(10000), new ThreadPoolExecutor.CallerRunsPolicy());
    }
}
复制代码
相关推荐
程序员小假5 小时前
我们来说一下 MySQL 的慢查询日志
java·后端
独自破碎E5 小时前
Java是怎么实现跨平台的?
java·开发语言
To Be Clean Coder5 小时前
【Spring源码】从源码倒看Spring用法(二)
java·后端·spring
xdpcxq10296 小时前
风控场景下超高并发频次计算服务
java·服务器·网络
想用offer打牌6 小时前
你真的懂Thread.currentThread().interrupt()吗?
java·后端·架构
橘色的狸花猫6 小时前
简历与岗位要求相似度分析系统
java·nlp
独自破碎E6 小时前
Leetcode1438绝对值不超过限制的最长连续子数组
java·开发语言·算法
用户91743965396 小时前
Elasticsearch Percolate Query使用优化案例-从2000到500ms
java·大数据·elasticsearch
计算机毕设VX:Fegn08956 小时前
计算机毕业设计|基于springboot + vue小区人脸识别门禁系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
yaoxin5211237 小时前
279. Java Stream API - Stream 拼接的两种方式:concat() vs flatMap()
java·开发语言