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

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();}}

相关文章:

  • 计算机视觉之三维重建(深入浅出SfM与SLAM核心算法)—— 1. 摄像机几何
  • Oracle DG库手动注册归档日志的两种方法
  • 【报错解决】RTX4090 nvrtc: error: invalid value for --gpu-architecture (-arch)
  • Android 手机操作系统的14个常见问题以及解决办法
  • PostgreSQL认证怎么选?PGCP中级认证、PGCM高级认证
  • Git 常用总结
  • 【Net】TCP/IP 协议
  • 《性能之巅》第十章 网络
  • 机器学习与深度学习20-数学优化
  • 如何彻底解决缓存击穿、缓存穿透、缓存雪崩
  • @Validation 的使用 Spring
  • LeetCode--29.两数相除
  • 【慧游鲁博】【13】后端 · 文物图片识别功能完善 · 个性化文物介绍
  • 火线、零线、地线 基础知识
  • Actix-web 中的权限中间件实现
  • 智慧养老与数字健康:科技赋能老年生活,构建全方位养老体系
  • 高防IP是怎么防御的?高防IP的防御步骤又有哪些?
  • 发布5大AI课程体系,传智教育破局AI开发人才荒
  • GitHub 趋势日报 (2025年06月11日)
  • 在MATLAB命令行执行ros2node 和 ros2subscriber后,执行ros2 topic list,MATLAB卡死
  • 手机网站首页模板/怎么制作网站教程
  • 沈阳市做网站电话/成都今天宣布的最新疫情消息
  • 网站后台无法更/seo网站快速整站优化技术
  • 黄村网站建设一条龙/免费下载百度并安装
  • 长沙3合1网站建设价格/网店推广分为哪几种类型
  • 找网站公司做网站/昆明装饰企业网络推广