当前位置: 首页 > ops >正文

Springboot实现Java程序和线程池的优雅关闭

下面会介绍三种关闭方法

1. Spring Boot中注册自定义的 JVM 停机钩子

package com.kira.scaffoldmvc.ShutDownHook;import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
@Slf4j
public class MySpringBootApp {public static void main(String[] args) {SpringApplication app = new SpringApplication(MySpringBootApp.class);app.addListeners(context -> {Runtime.getRuntime().addShutdownHook(new Thread(() -> {log.info("这是一个停机钩子方法");// 执行相关清理操作// 例如关闭消息队列连接// MqUtils.closeConnection();}));});app.run(args);}
}

通过 Runtime 类注册一个 Thread 作为停机钩子

这是JVM的一个钩子方法,我们需要注册钩子,注册完钩子后在JVM关闭的时候它不会直接关闭,而是去执行钩子方法,等钩子方法执行完后再关闭


2. @PreDestory针对特定bean关闭的时候做处理

@PreDestory是Bean销毁前方法,可以再Bean销毁前做处理,也就是关闭前处理

package com.kira.scaffoldmvc.ShutDownHook;import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Service;import java.sql.DriverManager;@Service
public class DatabaseService {private Connection connection;@PostConstructpublic void init() {// 初始化数据库连接this.connection = DriverManager.getConnection(url, username, password);}//标记Bean销毁前需要执行的方法@PreDestroypublic void cleanup() {// 应用关闭时自动释放数据库连接if (connection != null) {connection.close();log.info("Database connection closed");}}
}

3. 利用Spring的关闭事件-ContextClosedEvent

注册一个关闭事件ContextClosedEvent,将这个ApplicationListener<ContextClosedEvent>注册成bean

1.将关闭事件注册成Bean

package com.kira.scaffoldmvc.ShutDownHook;import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@SpringBootApplication
@Slf4j
public class GracefulShutdownApplication {public static void main(String[] args) {SpringApplication.run(GracefulShutdownApplication.class, args);log.info("Application started");}@RestController@RequestMapping("/api")static class SampleController {@GetMapping("/quick")public String quickRequest() {return "Quick response";}@GetMapping("/slow")public String slowRequest() throws InterruptedException {// 模拟长时间处理的请求log.info("Start processing slow request");Thread.sleep(10000); // 10秒log.info("Finished processing slow request");return "Slow response completed";}}//spring容器关闭时触发的事件@Beanpublic ApplicationListener<ContextClosedEvent> contextClosedEventListener() {return event -> log.info("Spring容器正在关闭");}
}

2.连接关闭事件接口

@Component
public class GracefulShutdownListener implements ApplicationListener<ContextClosedEvent> {@Overridepublic void onApplicationEvent(ContextClosedEvent event) {// 执行资源释放逻辑threadPool.shutdown();connectionPool.close();}
}

配置文件中如何开启优雅停机-阻止新请求进入Tomcat

spring:application:name: XXXdatasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://XXXX:3306/XXXusername: rootpassword: KIRAhikari:minimum-idle: 5            # ???????????maximum-pool-size: 20       # ?????????idle-timeout: 60000         # ????????????max-lifetime: 1800000       # ??????????connection-timeout: 20000   # ???????????????validation-timeout: 5000    # ?????????????leak-detection-threshold: 2000 # ????????????# 超时时间:等待存量请求完成的最大时间lifecycle:timeout-per-shutdown-phase: 30s
server:shutdown: graceful  # 启用优雅停机模式

为什么要开启优雅停机?

一般来说是停机的时候走我们的钩子方法

开启shutdown:graceful的时候,tomcat会停止接受新的请求,然后最多等待这个请求处理xx时间

然后等自定义的钩子方法shutdownHook执行完后,再关闭

如果不开启这个话,钩子方法处理的时候仍然会有新的请求进入tomcat


实战-实现线程池的优雅关闭

线程池注册成Bean
package com.kira.scaffoldmvc.ShutDownHook;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;@Configuration
public class ThreadPoolConfig {public static final int CORE_POOL_SIZE = 5;public static final int MAX_POOL_SIZE = 10;public static final int QUEUE_CAPACITY = 100;public static final Long KEEP_ALIVE_TIME = 1L;@Beanpublic ThreadPoolExecutor kiraExecutor1() {return new ThreadPoolExecutor(CORE_POOL_SIZE,MAX_POOL_SIZE,KEEP_ALIVE_TIME,TimeUnit.SECONDS,new ArrayBlockingQueue<>(QUEUE_CAPACITY),new ThreadPoolExecutor.AbortPolicy());}@Beanpublic ThreadPoolExecutor kiraExecutor2() {return new ThreadPoolExecutor(CORE_POOL_SIZE,MAX_POOL_SIZE,KEEP_ALIVE_TIME,TimeUnit.SECONDS,new ArrayBlockingQueue<>(QUEUE_CAPACITY),new ThreadPoolExecutor.AbortPolicy());}@Beanpublic ThreadPoolExecutor kiraExecutor3() {return new ThreadPoolExecutor(CORE_POOL_SIZE,MAX_POOL_SIZE,KEEP_ALIVE_TIME,TimeUnit.SECONDS,new ArrayBlockingQueue<>(QUEUE_CAPACITY),new ThreadPoolExecutor.AbortPolicy());}}

测试接口

往线程池里面添加任务

package com.kira.scaffoldmvc.ShutDownHook;import com.kira.scaffoldmvc.ShutDownHook.ThreadPoolConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicInteger;@RestController
@RequestMapping("")
public class ThreadPoolTaskController {@Autowiredprivate ThreadPoolExecutor kiraExecutor1;private final AtomicInteger taskCounter = new AtomicInteger(0);@GetMapping("/test")public String submitTasks() {final int TASK_COUNT = 100;long startTime = System.currentTimeMillis();try {// 提交100个任务到线程池for (int i = 0; i < TASK_COUNT; i++) {final int taskId = taskCounter.incrementAndGet();kiraExecutor1.execute(() -> {try {// 模拟任务执行,随机耗时50-200毫秒long sleepTime = (long) (Math.random() * 15000 + 50);Thread.sleep(sleepTime);// 打印任务完成信息System.out.println("任务 " + taskId + " 执行完成,耗时: " + sleepTime + "ms");} catch (InterruptedException e) {Thread.currentThread().interrupt();System.out.println("任务 " + taskId + " 被中断");}});}// 返回提交成功信息return "成功提交 " + TASK_COUNT + " 个任务到线程池,耗时: " + (System.currentTimeMillis() - startTime) + "ms";} catch (Exception e) {return "提交任务失败: " + e.getMessage();}}@GetMapping("/status")public String getThreadPoolStatus() {return "线程池状态: 活跃线程数=" + kiraExecutor1.getActiveCount()+ ", 队列任务数=" + kiraExecutor1.getQueue().size()+ ", 已完成任务数=" + kiraExecutor1.getCompletedTaskCount()+ ", 总任务数=" + kiraExecutor1.getTaskCount();}
}

1.shutdownhook()-利用JVM的关闭钩子

使用钩子方法shutdownhook()

存在问题:如果是正常没任务的时候,钩子方法是可以关闭线程池的。但是此时仍然有线程在执行线程池,那么钩子方法关闭线程池就会失败,他会直接中断不再轮询线程池的状态,从而使日志信息丢失

也不能保证线程池都shutdown(),因为它中断停止了

原本的日志信息应该是

关闭线程池1

轮询线程池1状态

线程池1任务全部完成,线程池1已完全关闭

关闭线程池2

轮询线程池2状态

线程池2任务全部完成,线程池2已完全关闭

但是他在关闭线程池1往下指令逻辑的时候,就抛出中断异常停止轮询了,也停止遍历其他线程池,导致其他线程池没有调用shutdown()方法,而且日志也不会输出线程池状态。

它不会继续去轮询,即使你自定义了继续轮询,这也只是重试机制,重试次数是有限的,无法恢复自动轮询

package com.kira.scaffoldmvc;import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;@SpringBootApplication
@Slf4j
public class ScaffoldMvcApplication {@Autowired(required = false)private Map<String, ThreadPoolExecutor> threadPoolExecutorMap;public static void main(String[] args) {ConfigurableApplicationContext context = SpringApplication.run(ScaffoldMvcApplication.class, args);// 获取应用实例ScaffoldMvcApplication application = context.getBean(ScaffoldMvcApplication.class);// 注册 JVM 关闭钩子Runtime.getRuntime().addShutdownHook(new Thread(() -> {log.info("JVM 关闭钩子触发,开始优雅关闭线程池...");application.shutdownAllExecutorServices();log.info("所有线程池已优雅关闭,所有任务执行完成");}));}/*** 优雅关闭所有线程池,确保所有任务执行完成*/public void shutdownAllExecutorServices() {if (threadPoolExecutorMap != null && !threadPoolExecutorMap.isEmpty()) {threadPoolExecutorMap.forEach((name, executor) -> {log.info("正在关闭线程池: " + name);shutdownExecutorServiceCompletely(name, executor);});}}/*** 优雅关闭线程池,确保所有任务执行完成* @param poolName 线程池名称* @param executor 线程池实例*/private void shutdownExecutorServiceCompletely(String poolName, ExecutorService executor) {// 停止接收新任务executor.shutdown();// 等待所有任务执行完成,不设置超时try {// 定期检查线程池状态while (!executor.awaitTermination(5, TimeUnit.SECONDS)) {// 输出剩余任务信息,方便监控if (executor instanceof ThreadPoolExecutor) {ThreadPoolExecutor threadPool = (ThreadPoolExecutor) executor;log.info("线程池[{}]关闭中: 活跃线程数={}, 队列任务数={}, 已完成任务数={}, 总任务数={}",poolName,threadPool.getActiveCount(),threadPool.getQueue().size(),threadPool.getCompletedTaskCount(),threadPool.getTaskCount());}}log.info("线程池[{}]已完全关闭,所有任务执行完成", poolName);} catch (InterruptedException ie) {// 被中断时,继续尝试关闭log.info("线程池[{}]关闭过程被中断,继续尝试关闭...", poolName);Thread.currentThread().interrupt();//将中断标志为设为true,方面后面逻辑拓展//当我们抛出错误后,为了保证这个线程池的任务执行完我们选择继续等待,而不是shutdownNow()// 注意:这里不调用shutdownNow(),确保任务完成}}}

2.@Predestroy-利用Bean的销毁前方法

可以成功关闭线程池,同时不需要人为自定义重试逻辑,因为使用这个方法不会出现上面的线程被打断的情况,所以可以正常运行

它不会像JVM关闭钩子那样被中断,能成功关闭所有的线程池

package com.kira.scaffoldmvc;import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;@SpringBootApplication
@Slf4j
public class ScaffoldMvcApplication {@Autowired(required = false)private Map<String, ThreadPoolExecutor> threadPoolExecutorMap;public static void main(String[] args) {ConfigurableApplicationContext context = SpringApplication.run(ScaffoldMvcApplication.class, args);// 获取应用实例ScaffoldMvcApplication application = context.getBean(ScaffoldMvcApplication.class);}/*** 优雅关闭所有线程池,确保所有任务执行完成*/public void shutdownAllExecutorServices() {if (threadPoolExecutorMap != null && !threadPoolExecutorMap.isEmpty()) {threadPoolExecutorMap.forEach((name, executor) -> {log.info("正在关闭线程池: " + name);shutdownExecutorServiceCompletely(name, executor);});}}/*** 优雅关闭线程池,确保所有任务执行完成* @param poolName 线程池名称* @param executor 线程池实例*/private void shutdownExecutorServiceCompletely(String poolName, ExecutorService executor) {// 停止接收新任务executor.shutdown();// 等待所有任务执行完成,不设置超时try {// 定期检查线程池状态while (!executor.awaitTermination(5, TimeUnit.SECONDS)) {// 输出剩余任务信息,方便监控if (executor instanceof ThreadPoolExecutor) {ThreadPoolExecutor threadPool = (ThreadPoolExecutor) executor;log.info("线程池[{}]关闭中: 活跃线程数={}, 队列任务数={}, 已完成任务数={}, 总任务数={}",poolName,threadPool.getActiveCount(),threadPool.getQueue().size(),threadPool.getCompletedTaskCount(),threadPool.getTaskCount());}}log.info("线程池[{}]已完全关闭,所有任务执行完成", poolName);} catch (InterruptedException ie) {// 被中断时,继续尝试关闭log.info("线程池[{}]关闭过程被中断,继续尝试关闭...", poolName);Thread.currentThread().interrupt();//将中断标志为设为true,方面后面逻辑拓展//当我们抛出错误后,为了保证这个线程池的任务执行完我们选择继续等待,而不是shutdownNow()// 注意:这里不调用shutdownNow(),确保任务完成}}// 同时保留@PreDestroy作为备选关闭方式@PreDestroypublic void onDestroy() {System.out.println("Spring容器销毁,开始关闭线程池...");shutdownAllExecutorServices();}}
http://www.xdnf.cn/news/13575.html

相关文章:

  • 暴雨服务器成功交付长沙市第四医院
  • 大麦逆向so
  • 第 87 场周赛:比较含退格的字符串、数组中的最长山脉、一手顺子、访问所有节点的最短路径
  • Fiori笔记
  • 华为云Flexus+DeepSeek征文 | 弹性算力实战:Flexus X实例自动扩缩容策略优化
  • Vue开发学习笔记:动态渲染自定义封装的uview-plus的Toast组件
  • LeetCode--29.两数相除
  • 位移传感器远程监控软件说明
  • 【从零学习JVM|第八篇】深入探寻堆内存
  • BERT vs BART vs T5:预训练语言模型核心技术详解
  • MySQL锁机制的优化和MVCC底层原理解释
  • 【 java 虚拟机知识 第二篇 】
  • Vue 生命周期详解(重点:mounted)
  • Tomcat线程模型
  • bash挖矿木马事件全景复盘与企业级防御实战20250612
  • 干货分享|JumpServer PAM特权账号管理功能详解
  • WPF将容器内的组件按比例缩放
  • RAG实战:基于LangChain的《肖申克的救赎》知识问答系统构建指南
  • 医疗集团级“人-机-料-法-环”全流程质控的医疗数据质控方案分析
  • Verilog基础:标识符的定义位置
  • Seedance:字节发布视频生成基础模型新SOTA,能力全面提升
  • Java虚拟机解剖:从字节码到机器指令的终极之旅(一)
  • DRG支付场景模拟器扩展分析:技术实现与应用价值
  • Windows 前端开发环境一键启动 (NVM + Yarn)
  • 第五十一天打卡
  • EtherCAT转CANopen网关与伺服器在汇川组态软件上的配置步骤
  • 【AI论文】Qwen3 嵌入:通过基础模型推进文本嵌入和重新排序
  • JavaWeb期末速成 样题篇
  • JSON 技术:从核心语法到编辑器
  • ruoyi框架添加开始事件自定义属性解释