springboot优雅shutdown时异步线程安全优化

前面针对graceful shutdown写了两篇文章
第一篇:
https://blog.csdn.net/chenshm/article/details/139640775
只考虑了阻塞线程,没有考虑异步线程
第二篇:
https://blog.csdn.net/chenshm/article/details/139702105
第二篇考虑了多线程的安全性,包括异步线程。

1. 为什么还需要优化呢?

因为第二篇的写法还不够优美,它存在以下缺陷。

  • 只在一个service bean 里面对ExecutorService做predestroy,只能对一个service类的异步线程提供安全保障,其他service类的异步业务需要重写predestroy的逻辑,造成代码冗余。
  • 异步方法的写法比较麻烦,其他程序员并不常用。现在用springboot的程序员喜欢用@Async注解,随时随地可以把方法变成异步执行。
    从架构师的角度考虑的话,写代码尽量满足多数情况可用,易用,最好还是全局有效的,让其他程序员专注于写业务代码。
    接下来让我们实现@Async注解的异步方法在app graceful shutdow时保持线程安全。

2. 代码优化

  • 确认graceful shutdown settings
    graceful shutdown settings for springboot

  • 添加第一个servcie 的异步方法

package com.it.sandwich.service.impl;

import com.it.sandwich.service.Demo2Service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

/**
 * @Author 公众号: IT三明治
 * @Date 2024/6/16
 * @Description:
 */
@Slf4j
@Service
@Component
public class Demo2ServiceImpl implements Demo2Service {
    @Override
    @Async
    public void feedUserInfoToOtherService(String userId) throws InterruptedException {
        for (int i = 0; i < 40; i++) {
            log.info("Demo2Service update {} login info to other services, service num: {}", userId, i+1);
            Thread.sleep(1000);
        }
    }
}
  • 添加第二个Servcie 的异步方法
package com.it.sandwich.service.impl;

import com.it.sandwich.service.Demo2Service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

/**
 * @Author 公众号: IT三明治
 * @Date 2024/6/16
 * @Description:
 */
@Slf4j
@Service
@Component
public class Demo1ServiceImpl implements Demo1Service {
    @Override
    @Async
    public void feedUserInfoToOtherService(String userId) throws InterruptedException {
        for (int i = 0; i < 35; i++) {
            log.info("Demo1Service update {} login info to other services, service num: {}", userId, i+1);
            Thread.sleep(1000);
        }
    }
}

添加两个@Async方法,验证全局生效。

  • api接口
package com.it.sandwich.controller;

import com.it.sandwich.base.ResultVo;
import com.it.sandwich.service.Demo1Service;
import com.it.sandwich.service.Demo2Service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * @Author 公众号: IT三明治
 * @Date 2024/6/16
 * @Description:
 */
@Slf4j
@RestController
@RequestMapping("/api")
public class DemoController {

    @Resource
    Demo1Service demo1Service;
    @Resource
    Demo2Service demo2Service;

    @GetMapping("/{userId}")
    public ResultVo<Object> getUserInfo(@PathVariable String userId) throws InterruptedException {
        log.info("userId:{}", userId);
        demo1Service.feedUserInfoToOtherService(userId);
        demo2Service.feedUserInfoToOtherService(userId);
        for (int i = 0; i < 30; i++) {
            log.info("updating user info for {}, waiting times: {}", userId, i+1);
            Thread.sleep(1000);
        }
        return ResultVo.ok();
    }
}
  • @Async有效的全局线程池配置
package com.it.sandwich.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;


/**
 * @Author 公众号: IT三明治
 * @Date 2024/6/16
 * @Description:
 */
@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2); // 设置核心线程数
        executor.setMaxPoolSize(5); // 设置最大线程数
        executor.setQueueCapacity(100); // 设置队列容量
        executor.setThreadNamePrefix("sandwich-async-pool-"); // 自定义线程名称前缀
        executor.setWaitForTasksToCompleteOnShutdown(true); // 设置线程池关闭时是否等待任务完成
        executor.setAwaitTerminationSeconds(60); // 设置等待时间,如果你需要所有异步线程的安全退出,请根据线程池内敢长线程处理时间配置这个时间
        return executor;
    }
}

3. 验证代码

  • 重启服务
  • call api
Administrator@USER-20230930SH MINGW64 /d/git/micro-service-logs-tracing
$ curl http://localhost:8080/api/sandwich
  • shutdown app(Ctrl+F2)
  • 验证日志
    查看日志前我们先分析一下代码,我们一个api请求里面一共有三个线程,一个阻塞线程,两个@Async注解修饰的异步线程。阻塞线程的循环计数日志从1到30,Demo1Service 异步线程的循环计数日志从1到35,Demo2Service异步线程的循环计数日志从1到40。我们期待的结果是提前shutdown之后三个线程的计数日志都完整打印出来。
    graceful shutdown logs for three threads

日志完美验证了我们的期待。 我设置的“sandwich-async-pool-”线程名前缀也在两个线程日志中体现了。进一步证明AsyncConfig对所有@Async注解修饰的异步线程全局有效。
这是为什么呢?

4. AsyncConfig配置代码分析

当我在Spring配置中通过@Bean定义了一个ThreadPoolTaskExecutor实例,并且在同一配置类或其他被扫描到的配置类中启用了@EnableAsync注解时,这个自定义线程池会自动与Spring的异步任务执行机制关联起来。这一过程背后的原理涉及到Spring的异步任务执行器(AsyncConfigurer接口)的自动配置和代理机制,具体原因如下:

  1. Spring的自动装配(Auto Configuration): Spring Boot利用自动配置(auto-configuration)机制来简化配置。当它检测到@EnableAsync注解时,会自动寻找并配置一个TaskExecutor(线程池)来执行@Async标记的方法。如果在应用上下文中存在多个TaskExecutor的Bean,Spring通常会选择一个合适的Bean作为默认的异步执行器。自定义的ThreadPoolTaskExecutor Bean由于是明确配置的,因此优先级较高,自然成为首选。
  2. AsyncConfigurer接口: 当我使用@EnableAsync时,实际上是在告诉Spring去查找实现了AsyncConfigurer接口的配置类。如果我没有直接实现这个接口并提供自定义配置,Spring会使用默认的配置。但是,如果我提供了自定义的ThreadPoolTaskExecutor Bean,Spring会认为这是我希望用于异步任务的线程池。
  3. Spring AOP代理: @Async注解的方法在运行时会被Spring的AOP(面向切面编程)机制代理。这个代理逻辑会检查是否有配置好的TaskExecutor,如果有(比如我自定义的ThreadPoolTaskExecutor),就会使用这个线程池来执行方法,从而实现了异步调用。
  4. Bean的命名和类型匹配: 默认情况下,Spring在查找执行器时会优先考虑那些名为taskExecutor的Bean,这也是为什么在配置ThreadPoolTaskExecutor时通常会使用这个名字。当然,即使不叫这个名字,也可以通过实现AsyncConfigurer接口并重写getAsyncExecutor方法来指定使用的线程池。

综上所述,自定义的ThreadPoolTaskExecutor之所以能成为Spring异步任务执行的默认线程池,是因为Spring的自动配置逻辑、AOP代理机制以及通过配置明确指定了这个线程池的使用。
至此,graceful shutdown已经可以使多线程,高并发的项目在做release的时候,线程安全性得到保障。 特别是一些长处理的schedul job项目(其中好多job为了提交效率,用了异步机制),经过这样优化之后,release的信心是不是增强了好多。
写文章不容易,如果对您有用,请点个关注支持一下博主再走。谢谢。
如果有更好见解的朋友,请在评论区给出您的指导意见,感谢!

  • 6
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
在 Spring Boot 中,可以通过优雅停止来停止接收新的请求,并等待当前正在执行的线程执行完毕。具体实现方式如下: 1. 首先,在应用启动创建一个线程池,用于处理请求。可以使用 Java 中的 ExecutorService 或 Spring 中的 ThreadPoolTaskExecutor。 2. 在 Spring Boot 应用中添加一个 ShutdownHook,当应用被关闭,会调用这个 ShutdownHook。 3. 在 ShutdownHook 中,首先停止接收新的请求,然后等待当前正在执行的线程执行完毕。可以通过设置一个标志位来实现停止接收请求,然后在等待线程执行完毕,使用 ExecutorService 或 ThreadPoolTaskExecutor 的 awaitTermination() 方法来等待所有线程执行完毕。 示例代码如下: ```java @Component public class GracefulShutdown implements ApplicationListener<ContextClosedEvent> { private ExecutorService executorService; // 线程池 private volatile int runningTasks = 0; // 正在执行的任务数 public GracefulShutdown(ExecutorService executorService) { this.executorService = executorService; } @Override public void onApplicationEvent(ContextClosedEvent event) { // 停止接收新的请求 stopAcceptingRequests(); // 等待所有任务执行完毕 waitForRunningTasksToFinish(); } private void stopAcceptingRequests() { // 设置标志位,停止接收新的请求 } private void waitForRunningTasksToFinish() { // 等待所有任务执行完毕 executorService.shutdown(); try { if (!executorService.awaitTermination(30, TimeUnit.SECONDS)) { // 如果线程池中还有任务在执行,强制关闭线程池 executorService.shutdownNow(); } } catch (InterruptedException e) { // 异常处理 } } @Async // 在另一个线程中执行任务 public void executeAsyncTask(Runnable task) { executorService.submit(() -> { try { task.run(); } finally { // 任务执行完毕后,正在执行的任务数减1 runningTasks--; } }); // 正在执行的任务数加1 runningTasks++; } } ``` 在上面的代码中,我们使用了 Spring 的 @Async 注解,将任务提交到线程池中异步执行,同也记录了正在执行的任务数。在应用关闭,我们可以通过设置标志位来停止接收请求,然后等待所有任务执行完毕。当线程池中没有正在执行的任务,应用会成功关闭。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IT三明治

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值