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

大家好,我是工藤学编程 🦉大二在读
作业侠系列最新文章😉Java实现聊天程序
SpringBoot实战系列🐷SpringBoot实战系列之从Async组件应用实战到ThreadPoolTaskExecutor⾃定义线程池
一起刷算法与数据结构最新文章🐷一起刷算法与数据结构-树篇1
环境搭建大集合环境搭建大集合(持续更新)

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

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

前言:

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

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

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

模拟代码如下:

@Service
public class NotifyServiceImpl implements NotifyService {
    @Override
    public void sendCode() {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

然后我们的接口如下:

@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

如何解决上⾯说的问题?

⾃定义线程池

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

实战⾃定义线程池

@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

否则报错:

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核⼼数
⾮固定,根据实际情况压测进⾏调整

本篇完!

  • 16
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 12
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

工藤学编程

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

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

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

打赏作者

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

抵扣说明:

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

余额充值