最近这些年,两种趋势不断地推动我们反思我们设计软件的方式。第一种趋势和应用运行的硬件平台相关,第二种趋势与应用程序的架构相关,尤其是它们之间如何交互。
Future 接口的语义与基本用法
Future 接口是在 Java 5 版本问世的,其设计初衷是 对将来某个时刻会发生的结果 进行建模,它建模得到一个异步计算,而这个计算会在将来某个时刻返回结果。想象一下,你下单点了一份外卖,订单详情提示你30min以后送到,在这30min时间里你可以不阻塞在等外卖小哥送餐,而是去做其它重要的事情。等到30min计时结束后,到前台取你所点的外卖。
Future 比更底层的 Thread 要容易使用,使用方法是将耗时操作封装在 Callable 对象,并将其提交给 ExecutorService。如下例所示,future.get() 是阻塞的,除非给它设置明确的超时时间,否则会一直阻塞直到返回。
ExecutorService executor = Executors.newCachedThreadPool();
Future<Double> future = executor.submit(new Callable<Double>() {
public Double call() {
return doSomeLongComputation();
}});
doSomethingElse(); // ===> 提交任务后,就可以去做其他计算了
try {
Double result = future.get(1, TimeUnit.SECONDS); // ===> 阻塞等待任务返回,超时1s
} catch (ExecutionException ee) {
// 计算抛出一个异常
} catch (InterruptedException ie) {
// 当前线程在等待过程中被中断
} catch (TimeoutException te) {
// 在Future对象完成之前超过已过期
}
Future 接口的局限性
Future 机制实现了最基本的异步任务提交框架,并提供接口(isDone)用于检测异步计算是否已经结束,以及获取计算结果。但这些特性还不足以应对日益增多的并发需求,例如很难用简洁的代码控制两个任务并发执行,在获取到它们各自返回值后进行运算。典型的场景还有以下这些:
- 将两个异步计算合并为一个 —— 这两个异步计算之间相互独立,同时第二个又依赖于第一个的结果。
- 等待 Future 集合中的所有任务都完成。
- 等待 Future 集合中最快结束的任务完成。
- 通过编程方式完成一个 Future 任务的执行(即以手工设定异步操作结果的方式)。
- 应对 Future 的完成事件(即当 Future 的完成事件发生时会收到通知,并能使用 Future 计算的结果进行下一步的操作,不只是简单地阻塞等待操作的结果)。
CompletableFuture 出现的背景
Java 8 的 CompletableFuture 彻底改变了我们处理异步任务的方式。它不仅解决了传统 Future 的痛点,更引入了一套强大的 函数式异步编程模型。
单任务
假设查询商品价格 calculatePrice 是一个耗时操作,那么,可以得到如下计算价格的代码,它返回一个 Future<Double> ,表示计算价格的任务。
public Future<Double> getPriceAsync(String product) {
CompletableFuture<Double> futurePrice = new CompletableFuture<>();
new Thread( () -> {
double price = calculatePrice(product);
futurePrice.complete(price);
}).start();
return futurePrice; // ===> 无需等待计算任务,直接返回 Future 对象
}
以下是如何使用这段计算价格逻辑的代码,在调用 futurePrice.get() 获取计算后的价格时,线程是阻塞的。
Shop shop = new Shop("BestShop");
Future<Double> futurePrice = shop.getPriceAsync("my favorite product");
// 执行更多任务,比如查询其他商店
doSomethingElse();
// 在计算商品价格的同时
try {
double price = futurePrice.get(); // ===> 阻塞式获取价格
System.out.printf("Price is %.2f%n", price);
} catch (Exception e) {
throw new RuntimeException(e);
}
使用工厂方法 supplyAsync 创建 CompletableFuture
可以简化掉手动创建 Thread 的过程,使代码更加简洁:
CompletableFuture.supplyAsync(() -> "Hello")
.thenApply(s -> s + " World") // 同步转换
.thenApplyAsync(String::toUpperCase) // 异步转换
.thenAccept(System.out::println); // 消费结果
回调
thenAccept 函数可用于 CompletableFuture 回调,当耗时任务返回时立刻触发。
findPricesStream("myPhone").map(f -> f.thenAccept(System.out::println));
超时控制
在超时后返回预设值。
future.completeOnTimeout(defaultValue, 2, TimeUnit.SECONDS);
future.orTimeout(1, TimeUnit.SECONDS); // Java 9+
多任务
并行执行
对同一商品,在不同商店里查询其价格,这是一个典型的并行调用场景。
public List<String> findPrices(String product) {
List<CompletableFuture<String>> priceFutures = shops.stream()
.map(shop -> CompletableFuture.supplyAsync(
() -> shop.getName() + " price is " + shop.getPrice(product)))
.collect(toList());
return priceFutures.stream()
.map(CompletableFuture::join)
.collect(toList());
}
可以注意到,代码里将原本一条链上的函数调用拆分成了两部分,分别调用 collect(toList()) 进行终结操作。这样做的原因是,Stream 的中间操作具有 惰性 特性,除非显式调用终端操作 collect,否则不会触发 map 中的 supplyAsync 来生成任务。因此需要先创建异步任务 CompletableFuture 列表,然后对列表执行 join 操作以触发。
如果需要依赖两个耗时操作的结果进行下一步运算,可以使用 thenCombile 函数。
// 合并两个独立的 CompletableFuture 对象
Future<Double> futurePriceInUSD =
CompletableFuture.supplyAsync(() -> shop.getPrice(product))
.thenCombine( // ===> 启动下一个耗时任务
CompletableFuture.supplyAsync(
() -> exchangeService.getRate(Money.EUR, Money.USD)),
(price, rate) -> price * rate // ===> 取上文两个耗时任务结果进行运算
);
依赖执行
已知“获取价格”、“获取优惠”分别都是耗时操作,且后者依赖于前者的结果。可以用 thenCompose 函数实现上述需求。
public List<String> findPrices(String product) {
List<CompletableFuture<String>> priceFutures =
shops.stream()
.map(shop -> CompletableFuture.supplyAsync(
() -> shop.getPrice(product), executor)) // 创建 CompletableFuture
.map(future -> future.thenApply(Quote::parse)) // 应用 parse
.map(future -> future.thenCompose(quote -> // 使用 thenCompose 继续组装存在依赖关系的耗时操作
CompletableFuture.supplyAsync(
() -> Discount.applyDiscount(quote), executor))) // 依赖前一步算出的 quote
.collect(toList()); // ===> 先启动收集,以生成任务
return priceFutures.stream()
.map(CompletableFuture::join) // ===> 等待任务完成
.collect(toList());
}
任务组合策略
可以设置任何一个任务完成后就触发,或者所有任务完成才触发。
// 合并两个独立任务
future1.thenCombine(future2, (res1, res2) -> res1 + res2);
// 顺序组合(前一个结果作为下一个输入)
future1.thenCompose(res -> createNewFuture(res));
// 等待所有任务完成
CompletableFuture.allOf(futures)
.thenRun(() -> /* 全部完成后的操作 */);
// 任一任务完成即触发
CompletableFuture.anyOf(futures)
.thenAccept(firstResult -> {});
异常传播
可以在任务当中统一处理异常,并且在发生异常时降级返回。
CompletableFuture.supplyAsync(() -> {
if (error) throw new RuntimeException();
return "OK";
})
.exceptionally(ex -> "Fallback") // 异常恢复
.handle((res, ex) -> { // 统一处理结果和异常
if (ex != null) return "ERROR";
return res.toUpperCase();
});
指定线程池
默认的线程池是 ForkJoinPool,可以根据任务具体情况,设置线程池的线程数。
// 指定专用线程池执行
Executor customPool = Executors.newFixedThreadPool(10);
CompletableFuture.supplyAsync(() -> intensiveTask(), customPool);
参考资料
- Java 8 in Action
- DeepSeek