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

发布于:2025-06-14 ⋅ 阅读:(24) ⋅ 点赞:(0)

下面会介绍三种关闭方法

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

}