shutdownhook 中不能直接调用 datasource.close(),因连接池 close() 非线程安全且要求无活跃连接;应先停业务线程、等待连接归还、再单次调用 close()。

ShutdownHook 里不能调用 DataSource.close() 直接关连接池
很多同学一上来就写个 Thread,在 Runtime.getRuntime().addShutdownHook() 里直接调用 dataSource.close(),结果发现连接池根本没释放,或者抛出 IllegalStateException。这是因为主流连接池(比如 HikariCP、Druid)的 close() 方法不是线程安全的,且要求调用时没有活跃连接——而 shutdown hook 触发时,可能还有请求正在归还连接、或事务未提交完。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 先主动停止业务线程:比如关闭 Web 容器的接收器(Tomcat 的
server.stop())、停掉定时任务调度器(ScheduledExecutorService.shutdownNow()) - 再等待活跃连接归还:HikariCP 提供
HikariDataSource.getHikariPoolMXBean().getActiveConnections()轮询判断,Druid 有DruidDataSource.getActiveCount() - 最后才调用
close(),且确保只调一次——重复 close 可能抛SQLException
钩子执行期间,Spring 的 @PreDestroy 和 DisposableBean 已失效
Spring 容器关闭时会按顺序触发 Bean 销毁逻辑,但这个过程依赖于 ConfigurableApplicationContext.close() 主动调用。而 JVM 收到 SIGTERM(如 k8s 的 kill -15)后,只会执行 shutdown hook,不会自动触发 Spring 上下文关闭流程。所以你在 Service 类里写的 @PreDestroy 方法,大概率根本不会执行。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 把连接池释放逻辑统一收口到一个显式注册的 shutdown hook 中,别依赖 Spring 生命周期回调
- 如果用 Spring Boot,可监听
ContextClosedEvent,但它和 shutdown hook 是两条路径,要确保不重复释放 - 避免在 hook 里调用
ApplicationContext.getBean()——此时容器已销毁,会抛IllegalStateException: No bean factory available
Linux 容器环境下,kill -15 不等于 JVM 正常退出
在 Docker 或 Kubernetes 中,kill -15 发给 Java 进程,JVM 确实会触发 shutdown hook,但前提是进程是 PID 1。如果用了 sh -c "java ..." 启动,实际 PID 1 是 shell,它不转发信号,JVM 根本收不到 SIGTERM,hook 就永远不会跑。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- Dockerfile 里用
exec java -jar app.jar,让 Java 进程真正成为 PID 1 - k8s 的
terminationGracePeriodSeconds要设够(至少 30 秒),给 hook 留出时间做连接池清理 - 加日志确认 hook 是否执行:
System.out.println("Shutdown hook triggered"),别只信代码写了就一定跑
HikariCP 的 shutdown() 和 close() 别混用
HikariCP 文档里写的是 close(),但它的 HikariDataSource 实际继承自 HikariConfig,内部有个 shutdown() 方法(public 但非接口声明),部分老版本示例误传了这个方法。调用 shutdown() 可能跳过资源清理步骤,导致连接泄漏。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 一律用
HikariDataSource.close(),它是标准的AutoCloseable实现 - 检查依赖版本:2.7.x 之后的 HikariCP,
shutdown()已标记为@Deprecated - 如果用了 Lombok 的
@Cleanup,注意它只对AutoCloseable生效,对shutdown()无效
最常被忽略的一点:连接池释放不是“调用 close 就完事”,而是“等所有连接归还 + 关闭内部线程池 + 清理 JMX 注册”。中间任何一步卡住(比如数据库网络延迟、连接被业务线程长期持有),都会让 shutdown hook 阻塞,最终被 OS 强杀——这时候连接池其实没真正释放。










