在一些业务场景下,我们需要使用CompletableFuture进行异步编程
如下例:新启动一个thread,异步执行calculatePrice(假设这是一个耗时操作),这样能够去执行一些其它子任务,防止主线程阻塞
public Future<Integer> getPriceAsync(String product) {
CompletableFuture<Integer> future = new CompletableFuture<>();
new Thread(() -> {
try {
// 整个代码段中,calculatePrice(product)可能因业务场景变动而变动
// 其它的都是基本上不会变动的
future.complete(calculatePrice(product)); // 将计算后的price放入future中
} catch (Exception e) {
// 若发生异常,将异常放入future中
// 如果不放入future,子线程中抛异常时,会导致主线程一直阻塞在future.get()
future.completeExceptionally(e);
}
}).start();
return future;
}
对于如上代码段,我们可以使用CompletableFuture.supplyAsync()进行替换,下面的代码段和上面的在能力是是完全等价的
public Future<Integer> getPriceSupplyAsync(String product) {
return CompletableFuture.supplyAsync(() -> calculatePrice(product));
}
parallelStream与CompletableFuture
注:PC为16个处理器
// 当shops有16个时,总耗时为1s多一点
// 说明所有的getPrice()都是并行的
private static void findPricesWithParallelStream() {
List<String> prices = shops.parallelStream()
// shop.getPrice() 会sleep 1s
.map(shop -> shop.getShopName() + " price is " + shop.getPrice("product"))
.collect(Collectors.toList());
}
// 当shops有16个时,总耗时为2s多一点 -- CompletableFuture默认池内最大线程数是核心数减1
// 并行流和CompletableFuture它们内部采用的是同样的通用线程池,默认都使用固定数目的线程,
// 具体线程数取决于Runtime.getRuntime().availableProcessors()的返回值
private static void findPriceWithFuture() {
long start = System.currentTimeMillis();
List<CompletableFuture<String>> pricesFuture = shops.stream()
.map(shop -> CompletableFuture.supplyAsync(() ->
shop.getShopName() + " price is " + shop.getPrice("product")))
.collect(Collectors.toList());
List<String> prices = pricesFuture.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
}
// 当shops为18时,并行流总耗时2s多,并行流不支持指定线程数
// 但当为CompletableFuture指定线程池时,总耗时降低到1s多
// 因为getPrice()绝大部分时间都在wait,通过指定线程数的方法,能够提升CPU利用率
private static void findPriceWithFuture() {
ExecutorService executors =
Executors.newFixedThreadPool(Math.min(shops.size(), 100),
r -> {
Thread thread = new Thread(r);
thread.setDaemon(true);
return thread;
});
List<CompletableFuture<String>> pricesFuture = shops.stream()
.map(shop -> CompletableFuture.supplyAsync(() -> shop.getShopName() + " price is " + shop.getPrice("product"), executors))
.collect(Collectors.toList());
List<String> prices = pricesFuture.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
}
使用并行流还是CompletableFutures:
- 如果是计算密集型的操作,并且没有I/O,推荐使用Stream接口,因为实现简单,同时效率也可能是最高的(如果所有的线程都是计算密集型的,那就没有必要创建比处理器核数更多的线程)。
- 如果并行的工作单元还涉及等待I/O的操作(包括网络连接等待),那么使用
CompletableFuture灵活性更好。这种情况不使用并行流的另一个原因是,处理流的流水线中如果发生I/O等待,流的延迟特性会让我们很难判断到底什么时候触发了等待。