一.SpringBoot中的异步操作
异步操作根据是否有返回值可以派生为Callable、Future两类接口,我们知道在阿里巴巴的开发规约中并不推荐直接从当前线程中实例化一个线程来进行异步操作,这样主要是考虑JVM线程资源是宝贵的开销,线程应当取之于“线程池”,用完即当归还于“线程池”,而线程池的生命周期也是交给Spring容器来托管最佳,如果每个请求都随意的挥霍线程资源,没有一个统一调度的容器池,服务器将不堪重负,因此线程池资源需要首先进行合理配置。
1.配置Springboot线程池
采用外部配置的形式将线程池参数进行初始化,然后注入到Spring容器中。
@Configuration
@EnableAsync
public class AsyncConfig {
@Autowired
private ExcutorProperties excutorProperties;
@Bean
public Executor taskExecutor() {
// Spring 默认配置是核心线程数大小为1,最大线程容量大小不受限制,队列容量也不受限制。
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数
executor.setCorePoolSize(excutorProperties.getCorePoolSize());
// 最大线程数
executor.setMaxPoolSize(excutorProperties.getMaxPoolSize());
// 队列大小
executor.setQueueCapacity(excutorProperties.getMaxPoolSize());
// 当最大池已满时,此策略保证不会丢失任务请求,但是可能会影响应用程序整体性能。
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setThreadNamePrefix("My ThreadPoolTaskExecutor-");
executor.initialize();
return executor;
}
}
这里需要注意下ThreadPoolTaskExecutor 饱和策略,有四种方式:
- ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
- ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务。这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。
- ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉。
- ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。
2.注解异步方法
模拟一个多异步请求的场景,一个搜索接口如下,根据用户输入的内容去反馈搜索结构
而具体的实现按照类型进行区分,可以分为文本、新闻、图片以及音乐等等类型,还可以继续扩展:
3.异步回调与阻塞等待
注意:
- 多个线程操作同一个变量需考虑线程安全问题,此处组装结果的Map就采用的
ConcurrentHashMap
结构。 - JDK1.8中CompletableFuture继承自Future接口,通过get()方法也能获取请求结果,但是为阻塞的。
- Boot编程常常会将登录态或Request信息放入ThreadLocal中,此处用了异步编程以后,在异步线程中无法获取主线程的信息,如果需要登录态信息则需通过参数往下传递(一次血泪Bug吐槽)。
4.测试验证
5.后续优化
异步线程耗时过长,在主线程上需加上超时时间控制。
最后附上Github源码地址:https://github.com/tisonkong/JavaAssemble/tree/master/basic
参考列表:
- SnailClimb.SpringBoot 异步编程指南
- 醉眼识朦胧.使用CompletableFuture优化你的代码执行效率