大家好,我是工藤学编程 🦉 | 大二在读 |
---|---|
作业侠系列最新文章😉 | 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失效情况
- 注解@Async的⽅法不是public⽅法
- 注解@Async的返回值只能为void或者Future
- 注解@Async⽅法使⽤static修饰也会失效
- 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
面试题:
- 请你说下 ThreadPoolTaskExecutor线程池 有哪⼏个重要参数,什么时候会创建线程
查看核⼼线程池是否已满,不满就创建⼀条线程执⾏任务,否则执⾏第⼆步。
查看阻塞队列是否已满,不满就将任务存储在阻塞队列中,否则执⾏第三步。
查看线程池是否已满,即是否达到最⼤线程池数,不满就创建⼀条线程执⾏任务,否则就按照策略处理⽆法执⾏的任务。
总结:先是CorePoolSize是否满⾜,然后是Queue阻塞队列是否满,最后才是MaxPoolSize是否满⾜
- ⾼并发下核⼼线程怎么设置?
分IO密集还是CPU密集
CPU密集设置为跟核⼼数⼀样⼤⼩
IO密集型设置为2倍CPU核⼼数
⾮固定,根据实际情况压测进⾏调整
本篇完!