CompletableFuture
需求
需求介绍
现在,需要实现一个商城的详情页,就如同京东、淘宝一样。在这个页面中,有一些不是行必要的信息和关键信息不是防止一个业务模块中的
如:商品的基本信息是放入shoping数据库,由商品的业务模块进行处理,商品介绍中的信息如介绍图片保存在image数据库中,由图片模块进行处理
需求解决
首先,可以使用基本是 思想对业务进行处理,即按照基本业务进行处理
两个基本的方法代表两个业务:查询商品和查询图片
public static void shopping(){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("查询到了商品的详细信息");
}
public static void image(){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("远程调用:查询到了商品图片的信息");
}
其中,远程调用因为网络的问题,所有需要的时间比本地调用的时间要长一些
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
Main.shopping();
Main.image();
long endTime = System.currentTimeMillis();
System.out.println("花费的时间:"+(endTime-startTime));
}
运行程序,所花费的时间是3s
运行结果:
查询到了商品的详细信息
远程调用:查询到了商品图片的信息
花费的时间:3024
需求 ->> 性能
解决 了基本的需求,现在需要将业务进行从需求到性能的改变。即如何能将这3s的时间进一步的缩短呢?我们可以做如下的思考
- 两个业务,一个业务是1s,一个业务是2s,这两个业务需求的时间是不变的。
- image的业务是非必要的,即如果调用偶尔超时,对整个业务来说是没什么影响的
以上两点,我们发现了一种解决的方式:新开一个线程,让这个线程去执行查询图片的业务,可以给这个线程定义一个超时时间,若在超时,则报一个异常。
Future
Future是在jdk1.5 的时候,JUC大神Doug Lea编写的一个用于异步计算和编排的一个接口。Future是一个接口,它由一个本源的实现类FutureTask
。
private static void m3() throws Exception {
FutureTask<Integer> futureTask = new FutureTask<>(() -> {
Main.image();
return 1;
});
long startTime = System.currentTimeMillis();
// 开另外一个线程去执行查询图片,因为查询顺序执行的,所以如果把这防止shopping后面,
// 则这个线程会让main停止1s后才去执行,体现不出效果
new Thread(futureTask,"t1").start();
Main.shopping();
// 程序在这个两秒中后不返回结果,则抛出异常
// 注意:程序在前面就已经开始执行了,所以到这个get()的时候,程序已经执行了1s左右了
// 所以,虽然这里是2s,但是不会超时
futureTask.get(2L, TimeUnit.SECONDS);
long endTime = System.currentTimeMillis();
System.out.println("花费的时间:"+(endTime-startTime));
}
程序的执行结果如下
FutureTask的执行结果
查询到了商品的详细信息
远程调用:查询到了商品图片的信息
花费的时间:2017
好了,现在程序的性能已经从3s -> >> 2s了,从业务的最短时间来说,2s已经是完成业务需要的最短时间了,因为本地线程的1s和新线程的1s是并行执行的。
缺点:
会调用get方法造成线程的阻塞,虽然由一个超时的get方法,但是这个超时不是线程的执行时间,而是调用get方法后的超时时间
业务升级
新的需求:
由于用户量的提升,新增了优惠劵、促销等活动。若商品的价格能够触发优惠卷的 话,展现出来的价格就是使用了优惠劵之后的价格。
优惠劵的业务模块是一个单独的业务模块,需要使用微服务调用
对应这个新的需求,如果使用FutureTask来做的话,需要考虑如下问题:
- 要将商品价格和优惠劵价格进行比较,得到一个显示的价格
- 商品和优惠劵都是必要的信息,和图片信息不一样
- 使用FutureTask的话,线程的超时时间不好控制,因为调用get方法可以传入一个超时时间,但是也会阻塞,而且超时时间是从调用get方法的时候开始计算的,即线程前面运行了多少时间无法确认,所以也无法在后面调用get方法
若要解决以上问题,我们想到了一个方法去解决:
- 用一个线程去执行优惠劵的信息,再用另外一个线程去查询价格信息,再得到两个信息都完成后,对两者进行运算
- 运算结束后将结果返回给主线程
- 严格的控制超时时间
这就引入了jdk1.8的新方法CompletableFuture
CompletableFuture
介绍
先看类图
可以看出来,CompletableFuture是实现了Future的,所以它由Future的全部东南,并且还实例了另外一个接口:ComPletionStage,所以它的功能比Future更加的强大。这也符合设计模式中的开闭原则
:扩展新类而不是修改旧类
简要的介绍一些CompletionStage接口:
- 在一个阶段的任务执行完后可以触发另外一个阶段任务的执行,类是与Linux的管道分隔符
|
- 提供了函数式编程的能力
- 一个阶段的执行可以被单个阶段所触发,也可以被多个阶段触发
模拟
用一下代码来解决下上面的需求
private static void m4() throws Exception {
// map模拟展示数据的vo;数值1表示从数据库中查询出的优惠劵;数值2表示查询商品的详细详细
Map<Integer,Integer> map = new HashMap<>();
// 新开一个线程了,这个线程暂停2s
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("查询结果为1");
return 1;
}).whenComplete((v, e) -> {
// 当上面的运行完后,没有异常则将数据存入map中
if (e == null) {
System.out.println("查询成功");
map.put(1, v);
}
}).exceptionally((e) -> {
e.printStackTrace();
return null;
});
// 主线程中向map中添加数据,暂停1s
Main.put(map);
// 将future阻塞,保证主线程不终止
future.get();
System.out.println(map.get(2) - map.get(1));
}
主线程的添加方法,暂停1s
public static Map<Integer,Integer> put(Map<Integer,Integer> map){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(2,2);
return map;
}
大家发现了,在主线程中也含有get(),这个get也是和Future的get一样会造成阻塞,那么CompletableFuture的优点在哪呢?
大家注意链式编程中的whenComplete
方法,这个方法是:当前面运行完后,有了结果就执行这个方法,两个参数值:返回结果和异常
。
此时,将whenComplete
中的方法修改一下,就可以看出优点了:
if (e == null) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException x) {
x.printStackTrace();
}
System.out.println("查询成功");
map.put(1, v);
}
这里加了一个1s的延迟,执行结果如下:
查询结果为1
查询成功
两者相减结果:1
执行时间==2099
若使用Future,则需要3s左右的时间
主要方法
关于thenCompose
和thenApply
的区别:
两者都是让A执行完后执行B,不同的是
这是thenApply的代码
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
return 100;
});
CompletableFuture<CompletableFuture<String>> f = future.thenApply(i -> {
return CompletableFuture.supplyAsync(() -> {
return (i * 10) + "";
});
});
System.out.println(f.get().get()); //"1000"
这是thenCompose的代码
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
return 100;
});
CompletableFuture<String> f = future.thenCompose( i -> {
return CompletableFuture.supplyAsync(() -> {
return (i * 10) + "";
});
});
System.out.println("main执行结束");
System.out.println(f.get()); //1000
可以看出,这两个的主要区别是返回的对象不同
- thenCompose:偏向于对应之前得到的结果,再对其进行异步编排,需要再次使用到CompletableFuture,而得到的结果可以变成一个新的CompletableFuture返回
- thenApply:偏向于对上面得到的结果进行处理,不用使用CompletableFuture
总结
CompletableFuture在可以使用到一些从多处获取数据的地方使用,从而提高程序性能。
附录
函数式编程
接口名称 | 方法名称 | 参数 | 返回值 |
---|---|---|---|
Runnable | run | 无参数 | 无返回值 |
Function | apply | 1个参数 | 有返回值 |
Consume | accept | 1个参数 | 无返回值 |
Supplier | get | 没有参数 | 有返回值 |
据的地方使用,从而提高程序性能。 |
附录
函数式编程
接口名称 | 方法名称 | 参数 | 返回值 |
---|---|---|---|
Runnable | run | 无参数 | 无返回值 |
Function | apply | 1个参数 | 有返回值 |
Consume | accept | 1个参数 | 无返回值 |
Supplier | get | 没有参数 | 有返回值 |
BiConsumer | accept | 2个参数 | 无返回值 |