下面会介绍三种关闭方法
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;
@PostConstruct
public void init() {
// 初始化数据库连接
this.connection = DriverManager.getConnection(url, username, password);
}
//标记Bean销毁前需要执行的方法
@PreDestroy
public 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容器关闭时触发的事件
@Bean
public ApplicationListener<ContextClosedEvent> contextClosedEventListener() {
return event -> log.info("Spring容器正在关闭");
}
}
2.连接关闭事件接口
@Component
public class GracefulShutdownListener implements ApplicationListener<ContextClosedEvent> {
@Override
public void onApplicationEvent(ContextClosedEvent event) {
// 执行资源释放逻辑
threadPool.shutdown();
connectionPool.close();
}
}
配置文件中如何开启优雅停机-阻止新请求进入Tomcat
spring:
application:
name: XXX
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://XXXX:3306/XXX
username: root
password: KIRA
hikari:
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;
@Bean
public ThreadPoolExecutor kiraExecutor1() {
return new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.AbortPolicy()
);
}
@Bean
public ThreadPoolExecutor kiraExecutor2() {
return new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.AbortPolicy()
);
}
@Bean
public 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 {
@Autowired
private 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作为备选关闭方式
@PreDestroy
public void onDestroy() {
System.out.println("Spring容器销毁,开始关闭线程池...");
shutdownAllExecutorServices();
}
}