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

news2024/11/25 1:01:23

前面针对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的信心是不是增强了好多。
写文章不容易,如果对您有用,请点个关注支持一下博主再走。谢谢。
如果有更好见解的朋友,请在评论区给出您的指导意见,感谢!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1830203.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Rewrite the Stars

文章目录 摘要1、引言2、相关工作3、重写星操作3.1、单层中的星操作3.2、扩展到多层3.3、特殊情况3.4、实证研究3.4.1、星操作的实证优越性3.4.2、决策边界对比3.4.3、扩展到无激活函数的网络 3.5、开放讨论与更广泛的影响 4、概念验证&#xff1a;StarNet4.1、StarNet架构4.2、…

HDU - 5651 xiaoxin juju needs help(Java JS Python C C++)

题目来源 Problem - 5651 (hdu.edu.cn) 题目描述 众所周知&#xff0c;小新是一位才华横溢的程序员。当他还是小学六年级的学生时&#xff0c;他就知道回文字符串了。 今年夏天&#xff0c;他在腾讯实习。一天&#xff0c;他的领导来找小新帮忙。他的领导给了他一个字符串&a…

LabVIEW程序的常见加密方式

LabVIEW程序的加密对于保护知识产权和敏感数据至关重要。本文将详细介绍LabVIEW程序常用的加密方式&#xff0c;包括VI加密、代码保护、文件加密和通信加密等&#xff0c;帮助开发者选择合适的加密方法来确保程序的安全性和完整性。 LabVIEW程序的常见加密方式 VI加密&#xf…

Fiddler抓包工具介绍

下载 下载:Web Debugging Proxy and Troubleshooting Tools|Fiddler 进去要填一个表 汉化版 百度网盘 请输入提取码 提取码&#xff1a;xq9t 下载过附件之后分别把两个文件 点开fiddler就ok了 配置https fiddler要想抓到https包(解密的),点击tools->options勾选三个对…

做好程序前设计

不要小看任何一道编程题目&#xff01;一定一定一定要想好之后再动手&#xff01;&#xff01;&#xff01; 带上你的草稿本&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xf…

DP:01背包问题

一、背包问题的概述 背包问题是⼀种组合优化的NP完全问题。 本质上是为了找出“带有限制条件的组合最优解” 1、根据物品的个数&#xff0c;分为如下几类&#xff1a; • 01背包问题&#xff1a;每个物品只有⼀个&#xff08;重点掌握&#xff09;• 完全背包问题&#xff1…

牛客热题:最长上升子序列(一)

&#x1f4df;作者主页&#xff1a;慢热的陕西人 &#x1f334;专栏链接&#xff1a;力扣刷题日记 &#x1f4e3;欢迎各位大佬&#x1f44d;点赞&#x1f525;关注&#x1f693;收藏&#xff0c;&#x1f349;留言 文章目录 牛客热题&#xff1a;最长上升子序列(一)题目链接方法…

用PHP来调用API给自己定制一个“每日新闻”

头条新闻汇聚了互联网上的时事动态&#xff0c;提供最新新闻动态、网络热门话题和视频更新等&#xff0c;覆盖社会、政治、体育、经济、娱乐、科技等多个领域&#xff0c;并不断刷新内容。企业应用这一接口后&#xff0c;可以快速吸引更多的用户访问自己的平台。即使是非新闻类…

WINUI——CommunityToolkit.Mvvm Messenger接收消息时报错:Cannot access a disposed object.

背景 WINUI开发时使用CommunityToolkit.Mvvm的Messemger让UI展示一些信息时出现错误&#xff1a; System.ObjectDisposedException:“Cannot access a disposed object. ObjectDisposed_ObjectName_Name” 详细见下述截图&#xff1a; 开发环境 WIN11 WINUI&#xff13; …

微信小程序开发系列(三十五)·自定义组件的属性properties

微信小程序开发系列&#xff08;三十四&#xff09;自定义组件的创建、注册以及使用&#xff08;数据和方法事件的使用&#xff09;_wx小程序组件开发-CSDN博客 目录 1. 组件的属性 2. 组件的使用 3. 细节描述 1. 组件的属性 Properties是指组件的对外属性&#xff0c;主…

Nginx之静态文件服务器的搭建

1.概述 静态文件服务器是指提供HTML文件访问或客户端 可直接从中下载文件的Web服务器。对于图片、 JavaScript或CSS文件等渲染页面外观的、不会动态改 变内容的文件&#xff0c;大多数网站会单独提供以静态文件服 务器的方式对其进行访问&#xff0c;实现动静分离的架构。 HTML…

ReactNative和Android通信

初始化一个RN项目以后&#xff0c;接下来想要让Android与React Native通信 写一个继承自ReactContextBaseJavaModule类的子类&#xff0c;重写getName方法 package com.awesomeprojectimport android.util.Log import android.widget.Toast import com.facebook.react.bridge.…

Java并发自测题

文章目录 一、什么是线程和进程?线程与进程的关系,区别及优缺点&#xff1f;二、为什么要使用多线程呢?三、说说线程的生命周期和状态?四、什么是线程死锁?如何预防和避免线程死锁?五、synchronized 关键字六、并发编程的三个重要特性七、JMM &#xff08;Java Memory Mod…

Android 自定义View

我们所有的试图都是起源于自定义View&#xff0c;包括ViewGroup也是继承于它&#xff0c;可以说它是视图组件之父。 我们可以从它的大致流程来分为四个部分&#xff1a; 构造方法&#xff0c;onMeasure&#xff0c;onLayout&#xff0c;onDraw 构造方法&#xff1a; 它主要有…

Java | Leetcode Java题解之第160题相交链表

题目&#xff1a; 题解&#xff1a; public class Solution {public ListNode getIntersectionNode(ListNode headA, ListNode headB) {if (headA null || headB null) {return null;}ListNode pA headA, pB headB;while (pA ! pB) {pA pA null ? headB : pA.next;pB …

Springmvc接收请求参数

如果你觉得这种限制很麻烦&#xff0c;你可以改为String 因为所有参数在接收的时候原值都是字符串

Mac | 崩溃分析

一、dump分析 1. 导入符号&#xff1a; ./import_pdb.sh libmedia_stream_ext.dylib.dSYM ./import_pdb.sh libowcr.framework.dSYM 2. 分析dump&#xff1a; ./analyze_dump.sh AE59D64F-0E1D-4A18-8DAF-C2C4D22F9FA6.dmp 3. 第 2 步骤 中会输出崩溃模块、崩溃线程及堆栈…

使用 Python 进行测试(5)测试的类型

总结 和我一起唱&#xff01; 冒烟测试&#xff0c;让你快速失败&#xff1b; 回归测试&#xff0c;不打破过去&#xff1b; 健全性检查&#xff0c;保留所拥有&#xff1b; 集成测试&#xff0c;处理副作用&#xff1b; 端到端&#xff0c;永无尽头&#xff01; 回测&#xf…

SwiftUI 6.0(Xcode 16)全新 @Entry 和 @Previewable 宏让开发妙趣横生

概览 如火如荼的 WWDC 2024 已进入第五天&#xff0c;苹果开发平台中众多海量新功能都争先恐后的喷薄欲出。 在这里就让我们从中挑两个轻松有趣的新功能展示给小伙伴们吧&#xff1a;它们分别是 全新的 Entry 和 Previewable 宏。 在本篇博文中&#xff0c;您将学到如下内容&a…

用React编写一个密码组件表单

theme: condensed-night-purple highlight: atelier-cave-light 背景介绍 我们在使用网站或者应用程序的登录界面或创建帐户界面时&#xff0c;往往避免不了需要用户输入密码这一步骤&#xff0c;而用户是否可以选择看见他们输入的密码是十分重要的一项功能。尤其是在当输入的…