CompletableFutur相关知识点

37 阅读2分钟

在一些业务场景下,我们需要使用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

  1. 如果是计算密集型的操作,并且没有I/O,推荐使用Stream接口,因为实现简单,同时效率也可能是最高的(如果所有的线程都是计算密集型的,那就没有必要创建比处理器核数更多的线程)。
  2. 如果并行的工作单元还涉及等待I/O的操作(包括网络连接等待),那么使用CompletableFuture灵活性更好。这种情况不使用并行流的另一个原因是,处理流的流水线中如果发生I/O等待,流的延迟特性会让我们很难判断到底什么时候触发了等待。