目前开发的代码工作得很正常。但是,如果价格计算过程中产生了错误会怎样呢?非常不幸,这种情况下会得到一个相当糟糕的结果:用于提示错误的异常会被限制在试图计算商品价格的当前线程的范围内,最终会杀死该线程,而这会导致等待get
方法返回结果的客户端永久地被阻塞。
客户端可以使用重载版本的get
方法,它使用一个超时参数来避免发生这样的情况。这是一种值得推荐的做法,应该尽量在代码中添加超时判断的逻辑,避免发生类似的问题。使用这种方法至少能防止程序永久地等待下去,超时发生时,程序会得到通知发生了TimeoutException
。不过,也因为如此,你不会有机会发现计算商品价格的线程内到底发生了什么问题才引发了这样的失效。为了让客户端能了解商店无法提供请求商品价格的原因,你需要使用CompletableFuture
的completeExceptionally
方法将导致CompletableFuture
内发生问题的异常抛出。对代码优化后的结果如下所示:
//抛出CompletableFuture内的异常
public Future<Double> getPriceAsync(String product) {
CompletableFuture<Double> futurePrice = new CompletableFuture<>();
new Thread( () -> {
try {
double price = calculatePrice(product);
futurePrice.complete(price);
} catch (Exception ex) {
futurePrice.completeExceptionally(ex);
}
}).start();
return futurePrice;
}
客户端现在会收到一个ExecutionException
异常,该异常接收了一个包含失败原因的Exception
参数,即价格计算方法最初抛出的异常。所以,举例来说,如果该方法抛出了一个运行时异常“product not available”,客户端就会得到像下面这样一段ExecutionException
:
java.util.concurrent.ExecutionException: java.lang.RuntimeException: product not available
at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2237)
at lambdasinaction.chap11.AsyncShopClient.main(AsyncShopClient.java:14)
... 5 more
Caused by: java.lang.RuntimeException: product not available
at lambdasinaction.chap11.AsyncShop.calculatePrice(AsyncShop.java:36)
at lambdasinaction.chap11.AsyncShop.lambda$getPrice$0(AsyncShop.java:23)
at lambdasinaction.chap11.AsyncShop$$Lambda$1/24071475.run(Unknown Source)
at java.lang.Thread.run(Thread.java:744)
使用工厂方法supplyAsync创建CompletableFuture
目前为止已经了解了如何通过编程创建CompletableFuture
对象以及如何获取返回值,虽然看起来这些操作已经比较方便,但还有进一步提升的空间,CompletableFuture
类自身提供了大量精巧的工厂方法,使用这些方法能更容易地完成整个流程,还不用担心实现的细节。比如,采用supplyAsync
方法后,可以用一行语句重写上面代码中的getPriceAsync
方法,如下所示:
public Future<Double> getPriceAsync(String product) {
return CompletableFuture.supplyAsync(() -> calculatePrice(product));
}
supplyAsync
方法接受一个生产者(Supplier)作为参数,返回一个CompletableFuture
对象,该对象完成异步执行后会读取调用生产者方法的返回值。生产者方法会交由ForkJoinPool
池中的某个执行线程(Executor)运行,但是你也可以使用supplyAsync
方法的重载版本,传递第二个参数指定不同的执行线程执行生产者方法。一般而言,向CompletableFuture
的工厂方法传递可选参数,指定生产者方法的执行线程是可行的,此外,上面代码中getPriceAsync
方法返回的CompletableFuture
对象和前面手工创建和完成的CompletableFuture
对象是完全等价的,这意味着它提供了同样的错误管理机制,而前者你花费了大量的精力才得以构建。
剩余部分中,我们会假设你无法控制Shop
类提供API
的具体实现,最终提供给你的API
都是同步阻塞式的方法。这也是当你试图使用服务提供的HTTP API
时最常发生的情况。你会学到如何以异步的方式查询多个商店,避免被单一的请求所阻塞,并由此提升你的“最佳价格查询器”的性能和吞吐量。
所以,你已经被要求进行“最佳价格查询器”应用的开发了,不过你需要查询的所有商店都如开始时介绍的那样,只提供了同步API
。换句话说,你有一个商家的列表,如下所示:
List<Shop> shops = Arrays.asList(new Shop("BestPrice"),
new Shop("LetsSaveBig"),
new Shop("MyFavoriteShop"),
new Shop("BuyItAll"));
你需要使用下面这样的签名实现一个方法,它接受产品名作为参数,返回一个字符串列表,这个字符串列表中包括商店的名称、该商店中指定商品的价格:
public List<String> findPrices(String product);
你的第一个想法可能是使用学习的Stream
特性。你可能试图写出类似下面这个清单中的代码(作为第一个方案,如果能想到这些已经相当棒了!)。
public List<String> findPrices(String product) {
return shops.stream()
.map(shop -> String.format("%s price is %.2f",
shop.getName(), shop.getPrice(product)))
.collect(toList());
}
这段代码看起来非常直白。现在试着用该方法去查询产品。此外,也请记录下方法的执行时间,通过这些数据,我们可以比较优化之后的方法会带来多大的性能提升,具体的代码清单如下:
long start = System.nanoTime();
System.out.println(findPrices("myPhone27S"));
long duration = (System.nanoTime() - start) / 1_000_000;
System.out.println("Done in " + duration + " msecs");
运行结果输出如下:
[BestPrice price is 123.26, LetsSaveBig price is 169.47, MyFavoriteShop price
is 214.13, BuyItAll price is 184.74]
Done in 4032 msecs
正如预期的,findPrices
方法的执行时间仅比4秒钟多了那么几毫秒,因为对这4个商店的查询是顺序进行的,并且一个查询操作会阻塞另一个,每一个操作都要花费大约1秒左右的时间计算请求商品的价格。怎样才能改进这个结果呢?
应该想到的第一个,可能也是最快的改善方法是使用并行流来避免顺序计算,如下所示:
public List<String> findPrices(String product) {
return shops.parallelStream()
.map(shop -> String.format("%s price is %.2f",
shop.getName(), shop.getPrice(product)))
.collect(toList());
}
运行代码,与上面的执行结果相比较:
[BestPrice price is 123.26, LetsSaveBig price is 169.47, MyFavoriteShop price
is 214.13, BuyItAll price is 184.74]
Done in 1180 msecs
看起来这是个简单但有效的主意:现在对四个不同商店的查询实现了并行,所以完成所有操作的总耗时只有1秒多一点儿。能做得更好吗?让我们尝试使用CompletableFuture
,将findPrices
方法中对不同商店的同步调用替换为异步调用。