首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >SpringBoot实战系列之从Async组件应用实战到ThreadPoolTaskExecutor⾃定义线程池

SpringBoot实战系列之从Async组件应用实战到ThreadPoolTaskExecutor⾃定义线程池

作者头像
工藤学编程
发布2025-12-22 09:16:35
发布2025-12-22 09:16:35
530
举报

大家好,我是工藤学编程 🦉

大二在读

作业侠系列最新文章😉

Java实现聊天程序

SpringBoot实战系列🐷

SpringBoot实战系列之从Async组件应用实战到ThreadPoolTaskExecutor⾃定义线程池

一起刷算法与数据结构最新文章🐷

一起刷算法与数据结构-树篇1

环境搭建大集合

环境搭建大集合(持续更新)


在本栏中,我们之前已经完成了: SpringBoot实战系列之发送短信验证码

内容速览: 1.前言 2.用Jmeter对之前代码进行压测 3.之前代码问题引出 4.Async原理与失效场景 5.实战Async异步请求并进行压测 6.使用Async异步请求之后出现的问题 7.自定义线程池解决Async异步请求问题实战 8.线程池面试题

前言:

前面我已经实战了发送短信验证码,但是由于是同步请求,所有会导致我们的接口响应速度也很慢,此时我们可以通过@Async注解实现异步请求

我们可以假设我们请求第三方短信验证码需要200ms,接下来我们编写以下代码来模拟用Jmeter对我们的接口进行测压

为啥不用之前的短信接口压测?因为钱遭不住呀!

模拟代码如下:

代码语言:javascript
复制
@Service
public class NotifyServiceImpl implements NotifyService {
    @Override
    public void sendCode() {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

然后我们的接口如下:

代码语言:javascript
复制
@RestController
@RequestMapping("/api/v1/notify")
@Slf4j
public class NotifyController {


    @Autowired
    private NotifyService notifyService;

    @GetMapping("test1")
    public JsonData send()  {
        notifyService.sendCode();
        log.info("发送成功 工藤学编程");
        return JsonData.buildSuccess();
    }

}

压测设置如下:

在这里插入图片描述
在这里插入图片描述

也就是说,我们会向这个接口发起100000次请求

压测结果如下:

在这里插入图片描述
在这里插入图片描述

可以看到100000次请求都成功,吞吐量为957

问题

由于发送短信涉及到⽹络通信, 因此sendMessage⽅法可能会有⼀些延迟. 为了改善⽤户体验, 我们可以使⽤异步发送短信的⽅法 什么是异步任务 异步调⽤是相对于同步调⽤⽽⾔的,同步调⽤是指程序按预定顺序⼀步步执⾏,每⼀步必须等到上⼀步执⾏完后才能执⾏,异步调⽤则⽆需等待上⼀步程序执⾏完即可执⾏ 多线程就是⼀种实现异步调⽤的⽅式 MQ也是⼀种宏观上的异步 使⽤场景 适⽤于处理log、发送邮件、短信……等 涉及到⽹络IO调⽤等操作 使⽤⽅式

  • 启动类⾥⾯使⽤@EnableAsync注解开启功能,⾃动扫描
  • 定义异步任务类并使⽤@Component标记组件被容器扫描,异步⽅法加上@Async 注意:@Async失效情况
  1. 注解@Async的⽅法不是public⽅法
  2. 注解@Async的返回值只能为void或者Future
  3. 注解@Async⽅法使⽤static修饰也会失效
  4. spring⽆法扫描到异步类,没加注解@Async 或@EnableAsync注解 调⽤⽅与被调⽅不能在同⼀个类

Async原理:

Spring 在扫描bean的时候会扫描⽅法上是否包含@Async注解,动态地⽣成⼀个⼦类(即proxy代理类),当这个有注解的⽅法被调⽤的时候,实际上是由代理类来调⽤的,代理类在调⽤时增加异步作⽤

因此其他失效场景

如果这个有注解的⽅法是被同⼀个类中的其他⽅法调⽤的,那么该⽅法的调⽤并没有通过代理类,⽽是直接通过原来的那个 bean,所以就失效了 所以调⽤⽅与被调⽅不能在同⼀个类,主要是使⽤了动态代理,同⼀个类的时候直接调⽤,不是通过⽣成的动态代理类调⽤ ⼀般将要异步执⾏的⽅法单独抽取成⼀个类类中需要使⽤@Autowired或@Resource等注解⾃动注⼊,不能⾃⼰⼿动new对象在Async ⽅法上标注@Transactional是没⽤的,但在Async⽅法调⽤的⽅法上标注@Transactional 是有效的

我们按照上面所述使用方式加好对应注解后,重启项目,再次压测: 压测结果

在这里插入图片描述
在这里插入图片描述

可以看到,吞吐量为9164,接近提升了10倍

异步调⽤-压测⾼QPS后的背后原因和问题拆解 使用了Async异步请求之后 现象:压测后很快跑完全部内容,这是因为都在线程池内部的阻塞 队列⾥⾯,此时极易出现问题

  • 极容易出现OOM,甚至导致消息丢失 因为当默认8个核⼼线程数占⽤满了之后, 新的调⽤就会进⼊队列, 最⼤值是Integer.MAX_VALUE,表现为没有执⾏

如果大家想测试下,可以设置下idea启动进程的jvm参数: -Xms50M -Xmx50M,再压测可以看会不会发生oom

直接使⽤ @Async 注解没指定线程池的话,即未设置TaskExecutor时 默认使⽤Spring创建ThreadPoolTaskExecutor

  • 核⼼线程数:8
  • 最⼤线程数:Integer.MAX_VALUE ( 21亿多)
  • 队列使⽤LinkedBlockingQueue
  • 容量是:Integer.MAX_VALUE
  • 空闲线程保留时间:60s
  • 线程池拒绝策略:AbortPolicy

如何解决上⾯说的问题?

代码语言:javascript
复制
⾃定义线程池

⼤家的疑惑 使⽤线程池的时候搞混淆ThreadPoolTaskExecutor和ThreadPoolExecutor ThreadPoolExecutor,这个类是JDK中的线程池类,继承⾃Executor,⾥⾯有⼀个execute()⽅法,⽤来执⾏线程,线程池主要提供⼀个线程队列,队列中保存着所有等待状态的线程,避免了创建与销毁的额外开销 ThreadPoolTaskExecutor,是spring包下的,是Spring为我们提供的线程池类 Spring异步线程池的接⼝类是TaskExecutor,本质还是java.util.concurrent.Executor 解决⽅式 spring会先搜索TaskExecutor类型的bean或者名字为taskExecutor的Executor类型的bean,所以我们最好来⾃定义⼀个线程池,加⼊Spring IOC容器⾥ ⾯,即可覆盖

实战⾃定义线程池

代码语言:javascript
复制
@Configuration
@EnableAsync
public class ThreadPoolTaskConfig {

    @Bean(name = "threadPoolTaskExecutor")
    public Executor threadPoolTaskExecutor() {
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        //线程池创建的核⼼线程数,线程池维护线程的最少数量,即使没有任务需要执⾏,也会⼀直存活
        //如果设置allowCoreThreadTimeout=true(默认false)时,核⼼线程会超时关闭
        threadPoolTaskExecutor.setCorePoolSize(16);
        //最⼤线程池数量,当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务
        //当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务⽽抛出异常
        threadPoolTaskExecutor.setMaxPoolSize(64);
        //缓存队列(阻塞队列)当核⼼线程数达到最⼤时,新任务会放在队列中排队等待执⾏

        threadPoolTaskExecutor.setQueueCapacity(124);

        //当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize
        //允许线程空闲时间60秒,当maxPoolSize的线程在空闲时间到达的时候销毁
        //如果allowCoreThreadTimeout=true,则会直到线程数量=0

        threadPoolTaskExecutor.setKeepAliveSeconds(30);

        //spring 提供的 ThreadPoolTaskExecutor 线程池是有setThreadNamePrefix() ⽅法的。
        //jdk 提供的ThreadPoolExecutor 线程池是没有setThreadNamePrefix() ⽅法的

        threadPoolTaskExecutor.setThreadNamePrefix("工藤学编程-");
// 任务的等待时间 如果超过这个时间还没有销毁就 强制销毁,以确保应用最后能够被关闭
        threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);

        // rejection-policy:当pool已经达到max size的时候,如何处理新任务
        // CallerRunsPolicy():交由调⽤⽅线程运⾏,⽐如main 线程;如果添加到线程池失败,那么主线程会⾃⼰去执⾏该任 务,不会等待线程池中的线程去执⾏
//AbortPolicy():该策略是线程池的默认策略,如果线程池队列满了丢掉这个任务并且抛出RejectedExecutionException异常。
//DiscardPolicy():如果线程池队列满了,会直接丢掉这个任务并且不会有任何异常
//DiscardOldestPolicy():丢弃队列中最⽼的任务,队列满了,会将最早进⼊队列的任务删掉腾出空间,再尝试加⼊队列
        threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        threadPoolTaskExecutor.initialize();
        return threadPoolTaskExecutor;
    }


}

更改后,压测结果:

在这里插入图片描述
在这里插入图片描述

吞吐量1187,虽然相比同步提高不大,但是总比发生OOM,甚至消息丢失要来的好的多,并且我们可以通过对不同参数设置进行压测,找到一个最佳的参数设置

注意:

corePoolSize必须小于maxPoolSize

否则报错:

代码语言:javascript
复制
Failed to instantiate [org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor]: Factory method 'threadPoolTaskExecutor' threw exception; nested exception is java.lang.IllegalArgumentException

面试题:

  1. 请你说下 ThreadPoolTaskExecutor线程池 有哪⼏个重要参数,什么时候会创建线程

查看核⼼线程池是否已满,不满就创建⼀条线程执⾏任务,否则执⾏第⼆步。 查看阻塞队列是否已满,不满就将任务存储在阻塞队列中,否则执⾏第三步。 查看线程池是否已满,即是否达到最⼤线程池数,不满就创建⼀条线程执⾏任务,否则就按照策略处理⽆法执⾏的任务。 总结:先是CorePoolSize是否满⾜,然后是Queue阻塞队列是否满,最后才是MaxPoolSize是否满⾜

  1. ⾼并发下核⼼线程怎么设置?

分IO密集还是CPU密集 CPU密集设置为跟核⼼数⼀样⼤⼩ IO密集型设置为2倍CPU核⼼数 ⾮固定,根据实际情况压测进⾏调整

本篇完!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-07-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档