CompletableFuture: 组合式异步编程
本章内容
- 创建异步计算、并获取最终的结果
- 使用非阻塞操作提升吞吐量
- 设计和实现异步API
- 如何以异步的方式使用同步API
- 如何对两个或多个异步操作进行流水线和合并操作
- 如何处理异步操作的完成状态
正常的业务代码编写中,我们通过会去调用第三方接口,然后完成处理相应的业务逻辑,但是如果第三方接口不影响正常业务的前提下,我们不需要调用第三方接口占据流程的太多时间,从而我们想并发的执行几个松耦合任务,充分利用CPU的核,创造最吞吐量
并发的执行我们可以考虑Future接口,尤其是他的新版实现CompletableFuture是处理并发的利器。
以下说明什么是前面所说的并行以及这章所说的并发
Future接口
Future接口在JAVA5的时候被引入,设计初衷是为了对某个时刻发生的结果建模,返回一个执行结果的引用,这个引用会被返回给调用方。相当于解放了那些潜在耗时的操作线程。相当于你拿衣服去给洗衣店,洗衣店会告诉你什么时候洗好(这就是一个future事件),衣服干洗的时候你就可以去做其他事情。这里我们只需要将耗时的操作封装一个Callable的对象中,再通过它提交给线程池ExecutorService,以下是JAVA8调用的案例
package com.java.lamdba.eight;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
public class FutureTest {
public void test() {
// 创建线程池
ExecutorService executorService = Executors.newCachedThreadPool();
Future<List<String>> future = executorService.submit(() -> {
// 注意这里是Callable() -> R
return this.getAllList();
});
this.testAfterMethod();
try {
List<String> result = future.get();
System.out.println(result);
} catch (InterruptedException e) {
System.out.println(e);
} catch (ExecutionException e) {
System.out.println(e);
}
}
public void testAfterMethod() {
System.out.println("测试添加的时候的异步处理");
}
public List<String> getAllList() {
List<String> list = new ArrayList(16);
list.add("1");
list.add("2");
list.add("3");
System.out.println("添加完毕");
return list;
}
public static void main(String[] args) {
for (int i = 0; i< 10; i++) {
new FutureTest().test();
}
}
}
上面的代码存在一个弊端,就是如果异步执行的时间很长,那么就会造成整体的执行时间很长,即使我们用了超时处理.future,get()带参数的方法,定义最长的等待时间
future 接口的局限性
通过前面的例子,我们知道future提供了检测异步计算是否已经结束的方法(isDone)方法等待异步操作结束,用来获取计算的结果。但是这些并不能让你写出简洁的并发代码。比如,我们很难表述Future结果之间的依赖性,如果我们需要将一个future的结果返回给另外一个future运算,这时候我们需要用到另外一个接口CompletableFuture
使用CompletableFuture构建异步应用
为了展示CompletableFuture的强大特性,我们先模拟一个场景
创建一个最佳价格查询器,会根据多个在线商店。根据产品给出最低的价格
这里,我们将学到几个很重要的技能
- 如何为客户提供异步的API
- 让你使用同步的api的代码是非阻塞代码。 你会了解如何用流水线将两个连接的异步操作合并为一个异步计算操作。这种情况肯定会出现,比如,要计算出该商品的原始价格,并且还附带着一个折扣的代码,最终计算出实际的价格,所以你不得不访问第二个远程折扣服务,查询该折扣代码对应的折扣率。
- 还会使用以响应式的方式处理异步操作的完成事件,以及随着各个商店返回它的价格,最佳价格查询器如何持续的更新商品的最佳推荐,而不是等待所有的商店返回他们各自的价格再去计算。
什么是同步API,什么是异步API?
同步API是对传统方法调用的一种称呼,调用方调用被调用方的运行中会等待,直到被调用方返回,这也就是阻塞式调用的由来
异步API 跟同步API相反,异步API在被调用方完成之前,将剩余的任务交给另外一个线程去做,这也是非阻塞式的由来。
实现异步API
我们先以模拟最佳价格起,开发第一版同步API以下是同步API
package com.java.lamdba.eight;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
public class Shop {
/**
* @param product 调用同步API
* @return
*/
public double getPrice(String product) {
return this.calculatePrice(product);
}
/**
* 被调用方延迟一秒
*/
public void delay() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
/**
* 计算结果
*
* @param product
* @return
*/
public double calculatePrice(String product) {
this.delay();
Random random = new Random();
return random.nextDouble() * product.charAt(0) + product.charAt(1);
}
/**
* 异步计算价格
*
* @param product
* @return
*/
public Future<Double> getAsyncPrice(String product) {
CompletableFuture<Double> doubleCompletableFuture = new CompletableFuture<>();
new Thread(() ->{
double price = this.calculatePrice(product);
doubleCompletableFuture.complete(price);
}).start();
return doubleCompletableFuture;
}
public static void main(String[] args) {
Shop shop = new Shop();
long startTime = System.currentTimeMillis();
Future<Double> future = shop.getAsyncPrice("my frist product");
Long endTime = (System.currentTimeMillis() - startTime) /1_000;
System.out.println("运行时间不会大于1秒" + endTime);
// 这里模拟执行多个任务
doSomeThing();
// 读取计算的价格,注意这里,如果取不到会一直阻塞
try {
Double result = future.get();
System.out.println("结果为"+ result);
}catch (Exception ex) {
}
Long endTime2 = (System.currentTimeMillis() - startTime) /1_000;
System.out.println("测试结束" + endTime2); }
/**
* 延迟两秒
*/
public static void doSomeThing() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
从结果来看,说明了,调用方调用方法并不会阻断自己的运行
异常处理
如果我们的代码没有异常,程序就会工作的很正常,但是如果价格计算getAsyncPrice发生了异常,那么主程序将永远不会得到返回结果,最终会永久阻塞。
虽然主程序在get的时候可以通过有时间的参数避免发生这种情况,但是线程发生的是什么异常,可以使用CompletableFuture的completeExceptionally方法将导致CompletableFuture内发生问
题的异常抛出。
/**
* 异步计算价格
*
* @param product
* @return
*/
public Future<Double> getAsyncPrice(String product) {
CompletableFuture<Double> doubleCompletableFuture = new CompletableFuture<>();
try {
new Thread(() ->{
double price = this.calculatePrice(product);
doubleCompletableFuture.complete(price);
}).start();
}catch (Exception ex) {
doubleCompletableFuture.completeExceptionally(ex);
}
return doubleCompletableFuture;
}
ֵ使用工厂方法supplyAsync创建CompletableFuture
supplyAsync方法接受一个生产者(Supplier)作为参数,返回一个CompletableFuture对象,该对象完成异步执行后会读取调用生产者方法的返回值。生产者方法会由ForkJoinPool池中指定某个线程执行,但是你也可以使用supplyAsync的重载方法,传入第二个参数指定不同的执行线程执行生产者的方法。我们将在后面讲述指定线程执行生产者的方法。
通过工厂方法获取CompletableFuture还有一个好处就是不需要异常抛出,跟前面的代码完成等价,但是前面的代码你花费了大量的代码
/**
* 使用工厂方法建造CompletableFuture
*
* @param product
* @return
*/
public Future<Double> getPriceAsync(String product) {
return CompletableFuture.supplyAsync(() -> this.calculatePrice(product));
}
让你的代码免受阻塞之苦
下面我们介绍的场景建立在调用API查询多个店铺商品价格的时候,有一个店铺的API卡住了~,但是这并不能影响你的最佳价格查询器的正常运行。
先来看一版会引起阻塞式的代码
/**
* 获取所有店铺的价格
*
* @param product product
* @return
*/
public static List<String> findPrices(String product) {
List<Shop> shops = Arrays.asList(new Shop("BestPrice"),
new Shop("LetsSaveBig"),
new Shop("MyFavoriteShop"),
new Shop("BuyItAll"));
return shops.stream().map(shop -> {
return String.format("店铺名为%s的价格为%s",shop.name,shop.getPrice(product));
}).collect(toList());
}
public static void testAsync2() {
long start = System.nanoTime();
System.out.println(findPrices("myPhone27S"));
long duration = (System.nanoTime() - start) / 1_000_000;
System.out.println("完成所有的查询需要" + duration + " 毫秒");
}
上述四家店铺每一个都等待了一秒,查完所有花费了4秒。这是正常的。
下面我们看一版使用并行流的操作
/**
* 获取所有店铺的价格
*
* @param product product
* @return
*/
public static List<String> findParallelPrices(String product) {
List<Shop> shops = Arrays.asList(new Shop("BestPrice"),
new Shop("LetsSaveBig"),
new Shop("MyFavoriteShop"),
new Shop("BuyItAll"));
return shops.parallelStream().map(shop -> {
return String.format("店铺名为%s的价格为%s",shop.name,shop.getPrice(product));
}).collect(toList());
}
这里只有1秒,说明并行流很好的优化了速度的问题。
ֵ用 CompletableFuture 发起异步请求
前面已经介绍了工厂方法supplyAsync创建CompletableFuture对象,下面把它利用起来。
/**
* 将所有的异步请求放在CompletableFuture中
*
* @param product
* @return
*/
public List<CompletableFuture<String>> getCompletableFuture(String product) {
List<Shop> shops = Arrays.asList(new Shop("BestPrice"),
new Shop("LetsSaveBig"),
new Shop("MyFavoriteShop"),
new Shop("BuyItAll"));
return shops.stream().map(shop ->
CompletableFuture.supplyAsync(() -> {
return String.format("店铺名为%s的价格为%s",shop.name,shop.getPrice(product));
})).collect(toList());
}
使用这种方式,你会得到一个List<CompletableFuture>,列表中的每个
CompletableFuture对象在计算完成后都包含商品的String类型的名称。但是,由于你用CompletableFutures实现的findPrices方法要求返回一个List,你需要等待所有的future执行完毕,将其包含的值抽取出来,填充到列表中才能返回。
可以用另外一种操作,再使用一条流水线添加map操作,对所有future对象进行Join操作,一个接一个地等待他们运行结束。注意这里的join方法是CompletableFuture的,跟future中的get中的方法不同时join不会抛出任何检测到的异常。使用join你不再需要使用try catch语句让你传递给map的第二个参数变得过于臃肿。
/**
* 等待所有CompletableFuture任务结束返回
*
* @param list
* @return
*/
public static List<String> getAllPrices(List<CompletableFuture<String>> list) {
return list.stream().map(CompletableFuture::join).collect(Collectors.toList());
}
我们发现这一版本的响应速度比并行流的执行还要慢,下面我们分析一下,这跟你的CPU的个数有很大的关系
寻找更好的解决方案
假设我们多了一家商店,我们看看结果如何
/**
* 获取所有店铺的价格
*
* @param product productfindParallelPrices
* @return
*/
public static List<String> findParallelPrices(String product) {
List<Shop> shops = Arrays.asList(new Shop("BestPrice"),
new Shop("LetsSaveBig"),
new Shop("MyFavoriteShop"),
new Shop("BuyItAll"),
new Shop("test"));
return shops.parallelStream().map(shop -> {
return String.format("店铺名为%s的价格为%s",shop.name,shop.getPrice(product));
}).collect(toList());
}
结果发现了需要2秒,我们再来看看CompletableFuture的五个商店的效果
/**
* 将所有的异步请求放在CompletableFuture中
*
* @param product
* @return
*/
public static List<CompletableFuture<String>> getCompletableFuture(String product) {
List<Shop> shops = Arrays.asList(new Shop("BestPrice"),
new Shop("LetsSaveBig"),
new Shop("MyFavoriteShop"),
new Shop("BuyItAll"),
new Shop("test"));
return shops.stream().map(shop ->
CompletableFuture.supplyAsync(() -> {
return String.format("店铺名为%s的价格为%s",shop.name,shop.getPrice(product));
})).collect(toList());
}
发现这跟并行流的效果一样,是因为它们内部
采用的是同样的通用线程池,默认都使用ڌ定数目的线程,具体线程数取决于Runtime.
getRuntime().availableProcessors()的返回值。然而,CompletableFuture具有一定的优势,因为它允许你对执行器(Executor)进行配置,尤其是线程池的大小,让它以更适合应
用需求的方式进行配置,满足程序的要求,这是并行流无法解决的难题
使用定制的执行器
/**
* 将所有的异步请求放在CompletableFuture中
*
* @param product
* @return
*/
public static List<CompletableFuture<String>> getCompletableFuture(String product) {
// 这里使用了一个守护线程,这种方式不会阻止程序的关停
Executor executor =
Executors.newFixedThreadPool(Math.min(5, 100),
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true);
return t;
}
});
List<Shop> shops = Arrays.asList(new Shop("BestPrice"),
new Shop("LetsSaveBig"),
new Shop("MyFavoriteShop"),
new Shop("BuyItAll"),
new Shop("test"));
return shops.stream().map(shop ->
CompletableFuture.supplyAsync(() -> {
return String.format("店铺名为%s的价格为%s",shop.name,shop.getPrice(product));
}, executor)).collect(toList());
}
现在你已经了解了如何利用CompletableFuture为你的用户提供异步API,以及如何将一个同步又缓慢的服务转换为异步的服务。不过到目前为止,我们每个Future中进行的都是单次的操作。下一节中,你会看到如何将多个异步操作结合在一起,以流水线的方式运行,从描述形式上,它与你在前面学习的Stream API有几分类似。
对多个异步任务进行流水线操作
假设我们的最佳价格查询器多了一个操作,提供折扣服务,并且每一个折扣代码对应不同的折扣率,你要是用一个枚举来实现这一想法。
package com.java.lamdba.eight;
public class Discount {
public enum Code {
NONE(0), SILVER(5), GOLD(10), PLATINUM(15), DIAMOND(20);
private final int percentage;
Code(int percentage) {
this.percentage = percentage;
}
}
}
/**
* @param product 调用同步API
* @return
*/
public String getPrice(String product) {
Double price = calculatePrice(product);
Random random = new Random();
Discount.Code code = Discount.Code.values()[random.nextInt(Discount.Code.values().length)];
return String.format("%s:%.2f:%s", name, price, code);
}
实现折扣服务
package com.java.lamdba.eight;
public class Discount {
public enum Code {
NONE(0), SILVER(5), GOLD(10), PLATINUM(15), DIAMOND(20);
private final int percentage;
Code(int percentage) {
this.percentage = percentage;
}
}
/**
* quote 转Discount
*
* @param quote
* @return
*/
public static String applyDiscount(Quote quote) {
return quote.getShopName() + " 价格是 " +
Discount.apply(quote.getPrice(),
quote.getDiscountCode());
}
private static double apply(double price, Code code) {
delay();
return (price * (100 - code.percentage) / 100);
}
private static void delay() {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
System.out.println("aa");
}
}
}
package com.java.lamdba.eight;
public class Quote {
private final String shopName;
private final double price;
private final Discount.Code discountCode;
public Quote(String shopName, double price, Discount.Code code) {
this.shopName = shopName;
this.price = price;
this.discountCode = code;
}
/**
* @param s 价格转折扣
* @return
*/
public static Quote parse(String s) {
String[] split = s.split(":");
String shopName = split[0];
double price = Double.parseDouble(split[1]);
Discount.Code discountCode = Discount.Code.valueOf(split[2]);
return new Quote(shopName, price, discountCode);
}
public String getShopName() {
return shopName;
}
public double getPrice() {
return price;
}
public Discount.Code getDiscountCode() {
return discountCode;
}
}
/**
* 顺序流
*
* @param product
* @return
*/
public static List<String> getDiscount(String product) {
List<Shop> shops = Arrays.asList(new Shop("BestPrice"),
new Shop("LetsSaveBig"),
new Shop("MyFavoriteShop"),
new Shop("BuyItAll"),
new Shop("test"));
return shops.stream().map(shop -> shop.getPrice(product)).map(Quote::parse).map(Discount::applyDiscount)
.collect(Collectors.toList());
}
总结:我们发现5个店铺都要用10秒,是因为我们查询每一个店铺的价格要5秒,然后查看一次折扣的价格又要5秒,并且我们是使用了stream底层依赖的线程数量固定的通用线程。所以我们可以使用自定义的任务执行器更充分利用cpu
构造同步和异步操作
让我们再次以CompletableFuture提供的特性,以异步的方式重新实现实现价格查询器
/**
* 用CompletableFuture重构代码
*
* @param product 产品名字
* @return
*/
public static List<String> getAsyncDiscount(String product) {
List<Shop> shops = Arrays.asList(new Shop("BestPrice"),
new Shop("LetsSaveBig"),
new Shop("MyFavoriteShop"),
new Shop("BuyItAll"),
new Shop("aaaa"));
Executor executor = Executors.newFixedThreadPool(Math.min(5, 100),
// 创建一个守护线程,不要阻塞程序的关闭
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true);
return t;
}
});
List<CompletableFuture<String>> priceFutures =
shops.stream()
.map(shop -> CompletableFuture.supplyAsync(() -> shop.getPrice(product), executor))
.map(future -> future.thenApply(Quote::parse))
.map(future ->
future.thenCompose(quote ->
CompletableFuture.supplyAsync(() ->
Discount.applyDiscount(quote), executor)))
.collect(toList());
return priceFutures.stream().map(CompletableFuture::join).collect(Collectors.toList());
}
注意到我们用了三个map
第一个map以异步方式取得每个shop中指定产品的原始价格
第二个map原始价格转折扣服务对象
第三个map使用另一个异步任务构造期望的future,申请折扣
我们再来看看程序执行的流程图
- 获取价格:这一步我们之前已经很熟悉了,是将lamdba表达式为参数传给supplyAsync工厂方法就可以异步对shop进行查询原始价格,并且这是一个用我们自己定义的执行器去执行的。
- 解析报价: 这里用到了applythen你可以把这看成是为处理CompletableFuture的结果建立了一个菜单,就像你曾经为Stream的流水线所做的事儿一样,异步执行结果回调
- 为计算折扣价格构造Future:毫无疑问我们这里会去调用api查询折扣,同样也是希望跟第一个操作一样异步执行,为了实现这个目标,我们把lamdba表达式传给了CompletableFuture的工厂方法supplyAsync,但是却用了两个不同的CompletableFutures进行建模,我希望的是他们两个CompletableFutures按顺序的执行。
比如
(1)从shop获取价格,并把价格转换为Quoto
(2)拿到返回的Quoto将其作为参数传递给Discount中转服务,计算最终的折扣
java8 CompletableFuture提供了名为thenCompose的方法,他就是专门为两个异步的流水线进行同步操作的方法。当第一个流水线的操作完成后调用thenCompose,并向其传递了一个对象是CompletableFuture,结果作为参数传递给了第二个流水线去操作,使用这种方式,即使Future在向不同的商铺收集报价,主线程还是能继续执行其他重要的操作。
这三次map操作的返回元素是一个列表,你就得到了List<CompletableFuture>,这时候你就可以使用join依次获取每个流水线的值拼接结果。
这里还要注意一个提供的thenComposeAsync: 这是异步调用第二条CompletableFuture,但是本例第二个CompletableFuture要等待第一个CompletableFuture返回的quote。所以无论使用哪个版本,结果都一样,但是开销却不一样。
将两个 CompletableFuture整合起来,无论它们是否存在
我们来看看第二个CompletableFuture不需要依赖第一个CompletableFuture的情况。
我们在价格查询器加上一个需求,有一家商家提供的价格是以欧元进行计价的,我们希望以人民币的方式提供给客户 可以使用异步的方式向商店查询商品的价格同时从远程的汇率服务那里查询欧元与人民币之间的汇率,当二者都结束时,将这两个结果结合起来,返回商品价格乘以当时的汇率,得到以美元计价的商品价格。用这种方式,你需要使用第三个CompletableFuture对象,当前面两个CompletableFuture计算出结果,并由BiFunction方法完成合并后,由它来完成最终结束的任务。代码清单如下
/**
*
* 两个future不影响的情况下进行异步操作
* @param product
* @return
*/
public static List<Double> getTurnRMBMoney(String product) {
List<Shop> shops = Arrays.asList(new Shop("BestPrice"),
new Shop("LetsSaveBig"),
new Shop("MyFavoriteShop"),
new Shop("BuyItAll"),
new Shop("test"));
List<CompletableFuture<Double>> list = shops.stream()
.map((shop) ->
CompletableFuture.supplyAsync(() ->
shop.calculatePrice(product)))
.map(future ->
future.thenCombine(CompletableFuture.supplyAsync(() ->
Money.getMoney(Money.EUR, Money.RMB)), (Double price, Double rate) ->
price * rate))
.collect(toList());
return list.stream().map(CompletableFuture::join).collect(toList());
}
注意这里会返回第三个CompletableFuture进行结尾操作,所以会有四个Future进行
对Future 和 CompletableFuture 的回顾
我们先来看看java7 的对异步任务操作的Future合并
响应CompletableFuture的completion事件
本章所看到的代码都是模拟调用API一秒的执行等待。但是在现实生活中,很有可能调用的api失败了,所以我们希望购买的商品在某些商店的查询速度比另一些商店更快,为了说明这个我们使用randomDelay替代原来的固定查询。
public static void randomDelay() {
int delay = 500 + random.nextInt(2000);
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
需求改变:只要有商品返回商品价格就在第一时间显示返回值,不再等那些未返回商店
对最佳价格查询器应用的优化
首先我们要认识到,我们的获取价格最后都是List转的stream的操作。所以可以考虑返回stream的流水线
/**
* findPrices方法返回一个由Future构成的流
*
* @param product 商品名称
* @return
*/
public static Stream<CompletableFuture<String>> findPricesStream(String product) {
Executor executor =
Executors.newFixedThreadPool(Math.min(5, 100),
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true);
return t;
}
});
List<Shop> shops = Arrays.asList(new Shop("BestPrice"),
new Shop("LetsSaveBig"),
new Shop("MyFavoriteShop"),
new Shop("BuyItAll"));
return shops.stream()
// 以异步方式取得每个shop中指定产品的原始价格
.map(shop ->
CompletableFuture.supplyAsync(() ->
shop.getPriceRandom(product), executor))
// 原始价格转折扣服务对象(注意里边用到了thenApply
// 表示某个任务执行完成后执行的动作,即回调方法,会将该任务的执行结果即方法返回值作为入参传递到回调方法中)
.map(future -> future.thenApply(Quote::parse))
// 使用另一个异步任务构造期望的future,申请折扣(注意里边用到了thenCompose
// 将上一条CompletableFuture执行结果quote作为参数传给了下一个CompletableFuture)
.map(future ->
future.thenCompose(quote ->
CompletableFuture.supplyAsync(() ->
Discount.applyDiscount(quote), executor)));
}
最后需要对这个流水线进行操作,这个操作其实特别简单,就是在每一个CompletableFuture上注册一个操作,这个操作会把每一个CompletableFuture完成执行后使用它的返回值,java8 API的thenAccept就提供了这个完美的操作。我们先简单的打印一下最后的操作
findPricesStream("myPhone").map(f -> f.thenAccept(System.out::println));
由 于thenAccept方法已经定义了如何处理CompletableFuture返回的结果,一旦CompletableFuture计算得到结果,它就返回一个CompletableFuture< Void >。
所以,map操作返回的是一个Stream<CompletableFuture>。对这个<CompletableFuture < Void >>对象,你能做的事非常有限,只能等待其运行结束,不过这也是我们期望的,我希望给最慢的商店一个机会,让他可以打印出自己的价格,为了实现这一目标,我们可以将Stream的所有CompletableFuture< Void >对象放到一个数组中
/**
* Strem<CompletableFuture> 转数据
* @param product
*/
public static void testCompletion(String product) {
long start = System.nanoTime();
CompletableFuture[] futures = findPricesStream(product)
.map(f ->
f.thenAccept(s ->
System.out.println(s + " (done in " + ((System.nanoTime() - start) / 1_000_000) + " msecs)")
))
.toArray(CompletableFuture[]::new);
CompletableFuture.allOf(futures).join();
}
allOf工厂方法接收一个由CompletableFuture构成的数组,数组中的所有Completable对象执行完成之后,它返回一个CompletableFuture< Void >对象。这意味着,如果你需
要等待最初Stream中的所有 CompletableFuture对象执行完毕,对 allOf方法返回的CompletableFuture执行join操作是个不错的主意。这个方法对“最佳价格查询器”应用也是
有用的,因为你的用户可能会疑惑是否后面还有一些价格没有返回,使用这个方法,你可以在执
行完毕之后打印输出一条消息“所以商铺返回了价格 ”。
然而在另一些场景中,你可能希望只要CompletableFuture对象数组中有任何一个执行完毕就不再等待,比如,你正在查询两个汇率服务器,任何一个返回了结果都能满足你的需求。在
这种情况下,你可以使用一个类似的工厂方法anyOf。该方法接收一个CompletableFuture对象构成的数组,返回由第一个执行完毕的CompletableFuture对象的返回值构成的CompletableFuture< Object >。