概述
异步是说,A发起一个操作后(一般都是比较耗时的操作,如果不耗时的操作就没有必要异步了),可以继续自顾自的处理它自己的事儿,不用干等着这个耗时操作返回。
异步是目的,而多线程是实现这个目的的方法。
在java中,异步都是通过Thread类开启一个线程的方式实现的。
初始化线程的四种方式
我们以后再业务代码里面,前三种启动线程的方式都不用。【将所有的多线程异步任务都交给线程池执行】
->且每个系统拥有一个或几个线程池,每个任务都从系统线程池中初始化线程;而不是每需要一个线程就创建一次线程池。
->当前系统中池只有一两个,每个异步任务,提交给线程池让他自己去执行就行。
1、继承Thread类;
package com.atguigu.gulimall.search.thread;
/**
* 本类说明:
* 测试 开启多线程实现异步的方式
* @author yuanhai
* @date 2022年03月06日
*/
public class ThreadTest {
public static void main(String[] args) {
/**
* 创建和初始化线程的方式:
* 1、继承Thread类;
*/
System.out.println("main.start......");
// 方式一:继承Thread类
Thread thread = new Thread01();
thread.start(); // 启动线程
System.out.println("main.end......");
}
public static class Thread01 extends Thread {
@Override
public void run() {
System.out.println("当前线程:"+Thread.currentThread().getId());
int i = 10/2;
System.out.println("运行结果:"+i);
}
}
}
2、实现Runnable接口;
package com.atguigu.gulimall.search.thread;
/**
* 本类说明:
* 测试 开启多线程实现异步的方式
* @author yuanhai
* @date 2022年03月06日
*/
public class ThreadTest {
public static void main(String[] args) {
/**
* 创建和初始化线程的方式:
* 2、实现Runnable接口;
*/
System.out.println("main.start......");
Runnable01 runnable01 = new Runnable01();
new Thread(runnable01).start();
System.out.println("main.end......");
}
public static class Runnable01 implements Runnable {
@Override
public void run() {
System.out.println("当前线程:"+Thread.currentThread().getId());
int i = 10/2;
System.out.println("运行结果:"+i);
}
}
}
3、实现Callable接口+FutureTask,可以拿到返回结果,可以处理异常;
package com.atguigu.gulimall.search.thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* 本类说明:
* 测试 开启多线程实现异步的方式
* @author yuanhai
* @date 2022年03月06日
*/
public class ThreadTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
/**
* 创建和初始化线程的方式:
* 3、实现Callable接口+FutureTask,可以拿到返回结果,可以处理异常;
*/
System.out.println("main.start......");
FutureTask<Integer> futureTask = new FutureTask<>(new Callable01());
new Thread(futureTask).start();
// 阻塞等待整个线程执行完成,获取返回结果
Integer integer = futureTask.get();
System.out.println("main.end......,返回结果:"+integer);
}
public static class Callable01 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("当前线程:"+Thread.currentThread().getId());
int i = 10/2;
System.out.println("运行结果:"+i);
return i;
}
}
}
4、通过线程池的方式;
package com.atguigu.gulimall.search.thread;
import java.util.concurrent.*;
/**
* 本类说明:
* 测试 开启多线程实现异步的方式
* @author yuanhai
* @date 2022年03月06日
*/
public class ThreadTest {
public static ExecutorService service = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws ExecutionException, InterruptedException {
/**
* 创建和初始化线程的方式:
* 4、通过线程池的方式;
* 给线程池直接提交任务
*/
//我们以后再业务代码里面,前三种启动线程的方式都不用。【将所有的多线程异步任务都交给线程池执行】
//当前系统中池只有一两个,每个异步任务,提交给线程池让他自己去执行就行
System.out.println("main.start......");
service.execute(new Runnable01()); // execute()没有返回值;
Future<Integer> integerFuture = service.submit(new Callable01()); // submit()可以获取返回值;
System.out.println("main.end......");
}
public static class Runnable01 implements Runnable {
@Override
public void run() {
System.out.println("当前线程:"+Thread.currentThread().getId());
int i = 10/2;
System.out.println("运行结果:"+i);
}
}
public static class Callable01 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("当前线程:"+Thread.currentThread().getId());
int i = 10/2;
System.out.println("运行结果:"+i);
return i;
}
}
}
总结
继承Thread类和实现Runnable接口的方式,不能得到返回值;
实现Callable接口的方式可以获取返回值;
继承Thread类、实现Runnable接口和实现Callable接口的方式都不能控制资源;
线程池的方式可以控制资源,也可以获取执行结果,以及捕获异常;
线程池详解
线程池的创建
方法一:使用Executors工具类:
ExecutorService service = Executors.newFixedThreadPool(_10);
方法二:使用原生的ThreadPoolExecutor类:
ThreadPoolExecutor executor = new ThreadPoolExecutor(_);
着重来看下方法二,原生的方式。创建时,new ThreadPoolExecutor();需要传参数,最多要传七个参数,这七个参数就是线程池的七大参数。
线程池的七大参数
七大参数
1)corePoolSize:核心线程数;
线程池创建好以后就准备就绪的线程数量,就等待来接受异步任务去执行;
只要线程池不销毁,那么池中一直存在这些数量的线程,即使线程空闲。
除非设置了 allowCoreThreadTimeOut 属性,这个属性是允许核心线程超时;
2)maximumPoolSize:最大线程数量;
池中允许的最大的线程数;用来控制资源;
3)keepAliveTime:存活时间;
当线程数大于核心线程数的时候,线程在最大多长时间没有接到新任务就会终止释放,最终线程池维持在 corePoolSize 大小;
如果当前的线程数量大于corePoolSize,释放空闲的线程(maximumPoolSize-corePoolSize);
什么时候释放? 只要线程空闲时间大于指定的keepAliveTime,就会释放;
4)unit:时间单位;
用于设定存活时间 keepAliveTime 的时间单位;
5)BlockingQueue
用来存储等待执行的任务,如果当前对线程的需求超过了 corePoolSize大小,就会放在这里等待空闲线程执行;
即:如果任务有很多,就会将目前多的任务放在队列里面,只要有线程空闲,就会去队列里面取出新的任务继续执行;
6)threadFactory:线程的创建工厂; 比如指定线程名称等;
7)RejectedExecutionHandler handler:拒绝策略;
如果线程满了(队列+线程池都满了),线程池就会使用拒绝策略,按照我们指定的拒绝策略拒绝执行任务;
七大参数的执行顺序(线程池运行流程)
工作顺序:
1、线程池创建,准备好核心线程数量的核心线程,准备接受任务;
2、新的任务进来,用 核心线程core 准备好的空闲线程执行;
2.1、如果core满了,就将再进来的任务放入阻塞队列中;
如果核心线程core有空闲,就会自己去阻塞队列获取任务执行;
2.2、阻塞队列满了,就直接开新线程执行,但是新线程最大只能开到 最大线程数量maximumPoolSize 指定的数量;
2.3、如果max满了,就用RejectedExecutionHandler拒绝任务;
2.4、如果max没满,都执行完成,有很多空闲了,
则在指定的存活时间keepAliveTime以后,释放maximumPoolSize-corePoolSize这些线程
细节:
1)阻塞队列可以选用new LinkedBlockingDeque<>();
它阻塞队列能保存的任务数量默认是Integer的最大值,这可能会内存不够;
所以要传入我们业务定制的数量,比如压力测试得到的结果等,比如 new LinkedBlockingDeque<>(100000);
2)线程工厂可以使用 Executors.defaultThreadFactory();
3)拒绝策略 可以使用 new ThreadPoolExecutor.AbortPolicy());
例1:
ThreadPoolExecutor executor = new ThreadPoolExecutor(5,
200,
10,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(100000),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
例2:
一个线程池 core 7; max 20 ,queue:50,100并发进来怎么分配的;
7个会立即得到执行,50个会进入队列,再开13个进行执行。剩下的30个就使用拒绝策略。
拒绝策略一般都是丢弃:AbortPolicy;如果不想抛弃还要执行,拒绝策略可以使用CallerRunsPolicy;
常见的四种线程池
1、newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若 无可回收,则新建线程。
2、newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
3、newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行。
4、newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务 按照指定顺序(FIFO, LIFO, 优先级)执行。
Executors.newCachedThreadPool(); // core是0,所有都可回收
Executors.newFixedThreadPool(); // 固定大小,core=max;都不可回收
Executors.newScheduledThreadPool(); // 定时任务的线程池
xecutors.newSingleThreadExecutor(); // 单线程的线程池,后台从队列里面获取任务,挨个执行
开发中为什么要使用线程池
降低资源的消耗:
通过重复利用已经创建好的线程降低线程的创建和销毁带来的损耗;
提高响应速度:
因为线程池中的线程数没有超过线程池的最大上限时,有的线程处于等待分配任务的状态,当任务来时无需创建新的线程就能执行;
提高线程的可管理性:
线程池会根据当前系统特点对池内的线程进行优化处理,减少创建和销毁线程带来的系统开销。无限的创建和销毁线程不仅消耗系统资源,还降低系统的稳定性,所以使用线程池进行统一分配;
CompletableFuture解决异步编排问题
CompletableFuture使用场景及简介
前面我们已经知道,开发中使用线程池来做多线程相关处理,但是多线程场景下,还会面临一个问题: 在业务复杂情况下,一 个异步调用可能会依赖于另一个异步调用的执行结果 。
比如:
查询商品详情页的逻辑比较复杂,有些数据还需要远程调用,必然需要花费更多的时间:
同时,1、2、3可以异步执行; 4、5必须 要等待1执行结束后才能执行; 4 、5、6可以异步执行;
这时,就需要使用CompletableFuture来实现。
CompletableFuture启动异步任务
1 创建异步对象
CompletableFuture提供了四个静态方法,来创建一个异步操作:
下面我们就来创建一个异步对象:
例,使用runAsync()方法,来创建一个异步对象,这里我们传入自己定义的线程池:
@Slf4j
public class ThreadTest {
public static ExecutorService executor = Executors.newFixedThreadPool(10);
public static void main(String[] args) {
System.out.println("main.start......");
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
}, executor);
System.out.println("main.end......");
}
}
例,使用supplyAsnc()方法,来创建一个异步对象,这里传入自己定义的线程池:
public class ThreadTest {
public static ExecutorService executor = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("main.start......");
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
return i;
}, executor);
Integer integer = future.get();
System.out.println("--获取线程运行返回的结果:"+integer);
System.out.println("main.end......");
}
}
这里,可以通过supplyAsync()返回的CompletableFuture对象的get()方法,拿到线程运行到返回值;
2 完成回调与异常感知
whenCompete虽然能得到异常信息,但是没法修改放回数据,而exceptionally可以感知异常,同时返回默认值;
例:使用whenCompete, whenCompete(t,action)方法中,参数t,是运行结果,action是出现的异常:
public class ThreadTest {
public static ExecutorService executor = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("main.start......");
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 2;
System.out.println("运行结果:" + i);
return i;
}, executor).whenComplete((res, exception) -> {
System.out.println("----异步任务成功完成了,结果是:"+res+";异常是:"+exception);
});
System.out.println("main.end......");
}
}
此时的运行结果:
main.start…… 当前线程:12 运行结果:5 ——异步任务成功完成了,结果是:5;异常是:null main.end……
例2,出现异常的情况:
public class ThreadTest {
public static ExecutorService executor = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("main.start......");
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 0;
System.out.println("运行结果:" + i);
return i;
}, executor).whenComplete((res, exception) -> {
System.out.println("----异步任务成功完成了,结果是:"+res+";异常是:"+exception);
});
System.out.println("main.end......");
}
}
运行结果:
main.start…… 当前线程:12 ——异步任务成功完成了,结果是:null;异常是:java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero main.end……
例3,使用exceptionally感知异常,并修改返回结果:
public class ThreadTest {
public static ExecutorService executor = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("main.start......");
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 0;
System.out.println("运行结果:" + i);
return i;
}, executor).whenComplete((res, exception) -> {
//虽然能得到异常信息,但是没法修改返回数据。
System.out.println("----异步任务成功完成了,结果是:"+res+";异常是:"+exception);
}).exceptionally(throwable -> {
//可以感知异常,同时返回默认值
return 10; // 当结果出现异常,返回一个默认值
});
Integer integer = future.get();
System.out.println("---最终得到的结果:"+integer);
System.out.println("main.end......");
}
}
3 handle方法(方法执行完成后的处理)
handle相关方法可以对方法执行完成后进行处理,无论是成功完成,还是失败完成;
例:
public class ThreadTest {
public static ExecutorService executor = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("main.start......");
/**
* 方法完成后的处理
*/
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 0;
System.out.println("运行结果:" + i);
return i;
}, executor).handle((res, thr) -> {
if (res != null) {
return res * 2;
}
if (thr != null) {
return 0;
}
return 0;
});
Integer integer = future.get();
System.out.println("---最终得到的结果:"+integer);
System.out.println("main.end......");
}
}
运行结果:
main.start…… 当前线程:12 —-最终得到的结果:0 main.end……
4 线程串行化方法
/*
线程串行化
1)、thenRun:不能获取到上一步的执行结果,无返回值
.thenRunAsync(() -> {
System.out.println(“任务2启动了…”);
}, executor);
2)、thenAcceptAsync;能接受上一步结果,但是无返回值
3)、thenApplyAsync:;能接受上一步结果,有返回值
*/
例1:,使用thenRunAsync():
public class ThreadTest {
public static ExecutorService executor = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("main.start......");
CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 4;
System.out.println("运行结果:" + i);
return i;
}, executor).thenRunAsync(() -> {
System.out.println("---任务2启动了");
}, executor);
System.out.println("main.end......");
}
例2,使用thenAcceotAsync():
public class ThreadTest {
public static ExecutorService executor = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("main.start......");
CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 4;
System.out.println("运行结果:" + i);
return i;
}, executor).thenAcceptAsync( res -> {
System.out.println("---任务2启动了:"+res);
},executor);
System.out.println("main.end......");
}
例3,使用thenApplyAsync() :
public class ThreadTest {
public static ExecutorService executor = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("main.start......");
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
System.out.println("当前线程:" + Thread.currentThread().getId());
int i = 10 / 4;
System.out.println("运行结果:" + i);
return i;
}, executor).thenApplyAsync(res -> {
System.out.println("---任务2启动了:" + res);
return "hello" + res;
}, executor);
System.out.println("main.end......"+future.get());
}
5 两任务组合之两个任务都要完成
public class ThreadTest {
public static ExecutorService executor = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("main.start......");
/**
* 两个都完成
*/
CompletableFuture<Integer> future01 = CompletableFuture.supplyAsync(() -> {
System.out.println("任务1线程:" + Thread.currentThread().getId());
int i = 10 / 4;
System.out.println("任务1结束:" + i);
return i;
}, executor);
CompletableFuture<String> future02 = CompletableFuture.supplyAsync(() -> {
System.out.println("任务2线程:" + Thread.currentThread().getId());
System.out.println("任务2结束:" );
return "Hello";
}, executor);
// runAfterBothAsync()
// future01.runAfterBothAsync(future02,() -> {
// System.out.println("---任务3开始");
// },executor);
// thenAcceptBothAsync()
// future01.thenAcceptBothAsync(future02,(f1, f2) -> {
// System.out.println("---任务3开始,之前的结果: f1:"+f1+",f2:"+f2);
// },executor);
// thenCombineAsync()
CompletableFuture<String> future = future01.thenCombineAsync(future02, (f1, f2) -> {
return f1 + ":" + f2 + " -> Haha";
}, executor);
System.out.println("main.end......");
}
}
6 两任务组合之有一个完成就执行任务3
7 多任务组合
使用案例
参考gulimall的gulimall-product模块下的ItemController:
参照SkuInfoService的item()方法;
首先,SkuInfoService的item()方法,使用了异步编排之前的代码如下:
@Override
public SkuItemVo item(Long skuId) {
SkuItemVo skuItemVo = new SkuItemVo();
//1、sku基本信息获取 pms_sku_info
SkuInfoEntity infoEntity = getById(skuId);
skuItemVo.setInfo(infoEntity);
Long spuId = infoEntity.getSpuId();
Long catalogId = infoEntity.getCatalogId();
//2、sku的图片信息 pms_sku_images
List<SkuImagesEntity> images = skuImagesService.getImagesBySkuId(skuId);
skuItemVo.setImages(images);
//3、获取spu的销售属性组合。
List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(spuId);
skuItemVo.setSaleAttr(saleAttrVos);
//4、获取spu的介绍 pms_spu_info_desc
SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(spuId);
skuItemVo.setDesp(spuInfoDescEntity);
//5、获取spu的规格参数信息。
List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(spuId, catalogId);
skuItemVo.setGroupAttrs(attrGroupVos);
return skuItemVo;
}
下面,使用异步编排来改造:
第一步,我们需要定义一个我们在项目中使用的线程池 MyThreadCongfig:
package com.atguigu.gulimall.product.config;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 本类说明:
* 线程池配置类
* @author yuanhai
* @date 2022年04月14日
*/
@Configuration
public class MyThreadConfig {
public ThreadPoolExecutor threadPoolExecutor () {
return new ThreadPoolExecutor(50,
200,
10,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(100000),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
}
}
当然,这样写是不严谨的,应当将线程池中的参数改为可配置的,将核心线程数,最大线程数等,放到配置类中,作为属性引入进来:
首先,定义一个线程池属性类ThreadPoolConfigProperties:
package com.atguigu.gulimall.product.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 本类说明:
*
* @author yuanhai
* @date 2022年04月14日
*/
@ConfigurationProperties(prefix = "gulimall.thread") // 通过此注解,与配置文件绑定
@Component
@Data
public class ThreadPoolConfigProperties {
private Integer coreSize;
private Integer maxSize;
private Integer keepAliveTime;
}
然后,在配置文件中,配置线程池属性:
gulimall.thread.core-size=20
gulimall.thread.max-size=200
gulimall.thread.keep-alive-time=10
然后,在线程池配置类中使用:
package com.atguigu.gulimall.product.config;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 本类说明:
* 线程池配置类
* @author yuanhai
* @date 2022年04月14日
*/
// 如果没有把ThreadPoolConfigProperties通过@Component注解加到容器中,就要通过@EnableConfigurationProperties注解开启这个类的配置
//@EnableConfigurationProperties(ThreadPoolConfigProperties.class)
@Configuration
public class MyThreadConfig {
@Bean
public ThreadPoolExecutor threadPoolExecutor (ThreadPoolConfigProperties pool) {
return new ThreadPoolExecutor(pool.getCoreSize(),
pool.getMaxSize(),
pool.getKeepAliveTime(),
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(100000),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
}
}
最后,异步编排后的SkuInfoService的item()方法:
// 注入我们自己自定义的线程池
@Autowired
ThreadPoolExecutor executor;
@Override
public SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException {
SkuItemVo skuItemVo = new SkuItemVo();
CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {
//1、sku基本信息获取 pms_sku_info
SkuInfoEntity infoEntity = getById(skuId);
skuItemVo.setInfo(infoEntity);
return infoEntity;
}, executor);
CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync((res) -> {
//3、获取spu的销售属性组合。
List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(res.getSpuId());
skuItemVo.setSaleAttr(saleAttrVos);
}, executor);
CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync(res -> {
//4、获取spu的介绍 pms_spu_info_desc
SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(res.getSpuId());
skuItemVo.setDesp(spuInfoDescEntity);
}, executor);
CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync(res -> {
//5、获取spu的规格参数信息。
List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId());
skuItemVo.setGroupAttrs(attrGroupVos);
}, executor);
//2、sku的图片信息 pms_sku_images
CompletableFuture<Void> imageFuture = CompletableFuture.runAsync(() -> {
List<SkuImagesEntity> images = skuImagesService.getImagesBySkuId(skuId);
skuItemVo.setImages(images);
}, executor);
//等到所有任务都完成
CompletableFuture.allOf(saleAttrFuture,descFuture,baseAttrFuture,imageFuture).get();
return skuItemVo;
}