了不起的Java-CompletableFuture组合异步编程

Posted 昕友软件开发

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了了不起的Java-CompletableFuture组合异步编程相关的知识,希望对你有一定的参考价值。

在多任务程序中,我们比较熟悉的是分支-合并框架的并行计算,他的目的是将一个操作(比如巨大的List计算)切分为多个子操作,充分利用CPU的多核,甚至多个机器集群,并行执行这些子操作。

而CompletableFuture的目标是并发(执行多个操作),而非并行,是利用CPU的核,使其持续忙碌,达成最大吞吐,在并发进行中避免等待远程服务的返回值,或者数据库的长时查询结果等耗时较长的操作,如果解决了这些问题,就能获得最大的并发(通过避免阻塞)。
而分支-合并框架只是并行计算,是没有阻塞的情况。

Future接口

Future接口用于对将来某个时刻发生的结果进行建模,它建模了一种异步计算,返回一个执行结果的引用,计算完成后,这个引用被返回给调用方,
在使用Future时,将耗时操作封装在一个Callable接口对象中,提交给ExecutorService。

public interface Future<V> {
    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    boolean isDone();
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

我们在异步执行结果获取时,设置了get过期时间2秒,否则,如果没有正常的返回值,会阻塞线程,非常危险。

import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class FutureDemo {

    public static void main(String[] args) {
        
        ExecutorService executor = Executors.newCachedThreadPool();
        Future<Integer> result = executor.submit(new Callable<Integer>() {
            public Integer call() throws Exception {
                Util.delay();
                return new Random().nextInt();
            }
        });
        doSomeThingElse();
        executor.shutdown();
        try {
            try {
                System.out.println("result:" + result.get(2,TimeUnit.SECONDS));
            } catch (TimeoutException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

    private static void doSomeThingElse() {
         System.out.println("Do Some Thing Else." );   
        
    }
}

因为Future的局限性,如

  • 难异步合并
  • 等待 Future 集合中的所有任务都完成。
  • 仅等待 Future 集合中最快结束的任务完成,并返回它的结果。

一是我们没有好的方法去获取一个完成的任务;二是 Future.get 是阻塞方法,使用不当会造成线程的浪费。解决第一个问题可以用 CompletionService 解决,CompletionService 提供了一个 take() 阻塞方法,用以依次获取所有已完成的任务。对于第二个问题,可以用 Google Guava 库所提供的 ListeningExecutorService 和 ListenableFuture 来解决。这些都会在后面的介绍。

CompletableFuture组合异步

单一操作异步

我们的演示程序是查询多个在线商店,把最佳的价格返回给消费者。首先先查询一个产品的价格(即单一操作异步)。

先建一个同步模型商店类Shop,其中delay()是一个延时阻塞。

calculatePrice(String)同步输出一个随机价格,
getPriceAsync(String)是异步获取随机价格,依赖calculatePrice同步方法,返回类型是Future<Double>

import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;

public class Shop {

    private final String name;
    private final Random random;

    public Shop(String name) {
        this.name = name;
        random = new Random(name.charAt(0) * name.charAt(1) * name.charAt(2));
    }

    public double getPrice(String product) {
        return calculatePrice(product);
    }

    private double calculatePrice(String product) {
        delay();
        return random.nextDouble() * product.charAt(0) + product.charAt(1);
    }

    public Future<Double> getPriceAsync(String product) {
        CompletableFuture<Double> futurePrice = new CompletableFuture<>();
        new Thread( () -> {
                    double price = calculatePrice(product);
                    futurePrice.complete(price);
        }).start();
        return futurePrice;
    }

    public String getName() {
        return name;
    }

}

delay()阻塞,延迟一秒,注释的代码是随机延迟0.5 - 2.5秒之间

    public static void delay() {
        int delay = 1000;
        //int delay = 500 + RANDOM.nextInt(2000);
        try {
            Thread.sleep(delay);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

调用单一查询同步:

public class ShopSyncMain {
    public static void main(String[] args) {
        Shop shop = new Shop("BestShop");
        long start = System.nanoTime();
        double price = shop.getPrice("my favorite product");
        System.out.printf("Price is %.2f%n", price);
        long invocationTime = ((System.nanoTime() - start) / 1_000_000);
        System.out.println("Invocation returned after " + invocationTime 
                                                        + " msecs");
        // Do some more tasks
        doSomethingElse();
        long retrievalTime = ((System.nanoTime() - start) / 1_000_000);
        System.out.println("Price returned after " + retrievalTime + " msecs");
      }

      private static void doSomethingElse() {
          System.out.println("Doing something else...");
      }
}

//结果:

Price is 123.26
Invocation returned after 1069 msecs
Doing something else...
Price returned after 1069 msecs

嗯,一秒出头,这符合delay()阻塞一秒的预期

调用单一查询异步操作:

public class ShopMain {

  public static void main(String[] args) {
    Shop shop = new Shop("BestShop");
    long start = System.nanoTime();
    Future<Double> futurePrice = shop.getPriceAsync("my favorite product");
    long invocationTime = ((System.nanoTime() - start) / 1_000_000);
    System.out.println("Invocation returned after " + invocationTime 
                                                    + " msecs");
    // Do some more tasks, like querying other shops
    doSomethingElse();
    // while the price of the product is being calculated
    try {
        double price = futurePrice.get();
        System.out.printf("Price is %.2f%n", price);
    } catch (ExecutionException | InterruptedException e) {
        throw new RuntimeException(e);
    }
    long retrievalTime = ((System.nanoTime() - start) / 1_000_000);
    System.out.println("Price returned after " + retrievalTime + " msecs");
  }

  private static void doSomethingElse() {
      System.out.println("Doing something else...");
  }

}

结果:
Invocation returned after 54 msecs
Doing something else...
Price is 123.26
Price returned after 1102 msecs
咦,异步反而时间多了,别忘了,大家都是执行一个方法,不存在多个并发(也没有多个顺序流操作),异步当然显示不出优势。

异步异常

new Thread方式新建线程可能会有个糟糕的情况:用于提示错误的异常被限制在当前线程内,最终会杀死线程,所以get是无法返回期望值,client调用方会被阻塞。
你当然可以重载get,要求超时过期时间,可以解决问题,但你也应该解决这个错误逻辑,避免触发超时,因为超时又会触发TimeoutException,client永远不知道为啥产生了异常。
为了让client获知异常原因,需要对CompletableFuture对象使用completeExceptionally方法,将内部错误异常跑出。
改写如下:

    public Future<Double> getPrice(String product) {
        new Thread(() -> {
            try {
                double price = calculatePrice(product);
                futurePrice.complete(price);
            } catch (Exception ex) {
                futurePrice.completeExceptionally(ex);
            }
        }).start();

使用工厂方法创建CompletableFuture对象

前面用门通过手动建立线程,通过调用同步方法创建CompletableFuture对象,还可以通过CompletableFuture的工厂方法来方便的创建对象,重写如下:

    public Future<Double> getPrice(String product) {
        return CompletableFuture.supplyAsync(() -> calculatePrice(product));
    }

通过原生代码,我们可以了解supplyAsync和runAsync的区别,runAsync没有返回值,只是运行一个Runnable对象,supplyAsync有返回值U,参数是Supplier函数接口,或者可以自定义一个指定的执行线程池。

    public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
        return asyncSupplyStage(asyncPool, supplier);
    }

    public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,
                                                       Executor executor) {
        return asyncSupplyStage(screenExecutor(executor), supplier);
    }

    public static CompletableFuture<Void> runAsync(Runnable runnable) {
        return asyncRunStage(asyncPool, runnable);
    }

    public static CompletableFuture<Void> runAsync(Runnable runnable,
                                                   Executor executor) {
        return asyncRunStage(screenExecutor(executor), runnable);
    }

 

并发单一任务异步

前面是单一的操作异步,异步的内涵不能显现,现在要查询一大堆的商店(并发)的同一产品的价格,然后对比价格。
商店List:

private final List<Shop> shops = Arrays.asList(new Shop("BestPrice"),
new Shop("LetsSaveBig"),
new Shop("MyFavoriteShop"),
new Shop("BuyItAll")/*,
new Shop("ShopEasy")*/);

使用三种方式处理多个商店的价格查询:

//顺序同步方式
    public List<String> findPricesSequential(String product) {
        return shops.stream()
                .map(shop -> shop.getName() + " price is " + shop.getPrice(product))
                .collect(Collectors.toList());
    }

    //并行流方式
    public List<String> findPricesParallel(String product) {
        return shops.parallelStream()
                .map(shop -> shop.getName() + " price is " + shop.getPrice(product))
                .collect(Collectors.toList());
    }

    //工厂方法异步
    public List<String> findPricesFuture(String product) {
        List<CompletableFuture<String>> priceFutures =
                shops.stream()
                .map(shop -> CompletableFuture.supplyAsync(() -> shop.getName() + " price is "
                        + shop.getPrice(product), executor))
                .collect(Collectors.toList());

        List<String> prices = priceFutures.stream()
                .map(CompletableFuture::join)
                .collect(Collectors.toList());
        return prices;
    }

     private final Executor executor = Executors.newFixedThreadPool(shops.size(), new ThreadFactory() {
     @Override
     public Thread newThread(Runnable r) {
            Thread t = new Thread(r);
            t.setDaemon(true);
            return t;
        }
    });

CompletableFuture::join的作用是异步的开启任务并发,并且将结果合并聚集。

结果:
sequential done in 4130 msecs
parallel done in 1091 msecs
composed CompletableFuture done in 1010 msecs

如果将list个数变成5个,结果是
同步必然是线性增长
sequential done in 5032 msecs
并行流受到PC内核数的限制(4个),所以一组并行只能指派最多4个线程,第二轮要多费时1秒多
parallel done in 2009 msecs
CompletableFuture异步方式能保持1秒的秘密在于线程池,我们定义了shops.size()大小的线程池,并且使用了守护线程。java提供了俩类的线程:用户线程和守护线程(user thread and Daemon thread)。用户线程是高优先级的线程。JVM虚拟机在结束一个用户线程之前,会先等待该用户线程完成它的task。
在另一方面,守护线程是低优先级的线程,它的作用仅仅是为用户线程提供服务。正是由于守护线程是为用户线程提供服务的,仅仅在用户线程处于运行状态时才需要守护线程。另外,一旦所有的用户线程都运行完毕,那么守护线程是无法阻止JVM退出的

并发多任务异步

假设所有商店开始折扣服务,用5种折扣码代表。

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;
        }
    }

    public static String applyDiscount(Quote quote) {
        return quote.getShopName() + " price is " + Discount.apply(quote.getPrice(), quote.getDiscountCode());
    }
    private static double apply(double price, Code code) {
        delay();
        return format(price * (100 - code.percentage) / 100);
    }
}

public class Quote {

    private final String shopName;
    private final double price;
    private final Discount.Code discountCode;

    public Quote(String shopName, double price, Discount.Code discountCode) {
        this.shopName = shopName;
        this.price = price;
        this.discountCode = discountCode;
    }

    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;
    }
}

比如,SILVER(5)代表95折, GOLD(10)代表90折,PLATINUM(15)代表85折。、
Quote类用于包装输出。同步的价格计算也做了修改,使用:来分割商店名、价格、折扣码。折扣码是随机产生的。
新的商店类,getPrice同步方法输出商店名、价格、折扣码的待分割字符串。parse用于将字符串转化为Quote实例。
打折延迟1秒。

public class Shop {

    private final String name;
    private final Random random;

    public Shop(String name) {
        this.name = name;
        random = new Random(name.charAt(0) * name.charAt(1) * name.charAt(2));
    }

    public String getPrice(String product) {
        double price = calculatePrice(product);
        Discount.Code code = Discount.Code.values()[random.nextInt(Discount.Code.values().length)];
        return name + ":" + price + ":" + code;
    }

    public double calculatePrice(String product) {
        delay();
        return format(random.nextDouble() * product.charAt(0) + product.charAt(1));
    }

    public String getName() {
        return name;
    }
}

运行三种服务

public class BestPriceFinder {

    private final List<Shop> shops = Arrays.asList(new Shop("BestPrice"),
                                                   new Shop("LetsSaveBig"),
                                                   new Shop("MyFavoriteShop"),
                                                   new Shop("BuyItAll"),
                                                   new Shop("ShopEasy"));

    private final Executor executor = Executors.newFixedThreadPool(shops.size(), new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r);
            t.setDaemon(true);
            return t;
        }
    });

    //同步流
    public List<String> findPricesSequential(String product) {
        return shops.stream()
                .map(shop -> shop.getPrice(product))
                .map(Quote::parse)
                .map(Discount::applyDiscount)
                .collect(Collectors.toList());
    }
    //并行流
    public List<String> findPricesParallel(String product) {
        return shops.parallelStream()
                .map(shop -> shop.getPrice(product))
                .map(Quote::parse)
                .map(Discount::applyDiscount)
                .collect(Collectors.toList());
    }

    //CompletableFuture异步
    public List<String> findPricesFuture(String product) {
        List<CompletableFuture<String>> priceFutures = findPricesStream(product)
                .collect(Collectors.<CompletableFuture<String>>toList());

        return priceFutures.stream()
                .map(CompletableFuture::join)
                .collect(Collectors.toList());
    }

    public Stream<CompletableFuture<String>> findPricesStream(String product) {
        return 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)));
    }

    public void printPricesStream(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(size -> new CompletableFuture[size]);
        CompletableFuture.allOf(futures).join();
        System.out.println("All shops have now responded in " + ((System.nanoTime() - start) / 1_000_000) + " msecs");
    }

}
//结果
sequential done in 10176 msecs
parallel done in 4010 msecs
composed CompletableFuture done in 2016 msecs

 

  • 同步的10多秒是在5个list元素时获得的,每个一次询价,一次打折延迟,共2秒。
  • 并行流一轮需要2秒多(第一轮询价4个+1个共两个轮次,2秒,第二轮打折4个+1个)。
  • 异步方式第一轮和第二轮都是1秒多,所以2秒多。

这里的三步需要特别说明,这也是并发多任务的核心所在,并发是5条记录并发,多任务是

  1. 询价任务(耗时1秒)
  2. 解析字符换成Quote实例
  3. 计算折扣

这三个子任务

  • .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)));

第一行是用异步工厂执行同步方法getPrice,返回字符串,涉及线程池;
第二行thenApply是将字符串转成Quote实例,这个是同步内存处理,不涉及线程池;
thenapply()是接受一个Function<? super T,? extends U> fn参数用来转换CompletableFuture,相当于流的map操作,返回的是非CompletableFuture类型,它的功能相当于将CompletableFuture<T>转换成CompletableFuture<U>.
在这里是把CompletableFuture<String>转成CompletableFuture<Quote>
第三行也是异步处理,也涉及线程池,获得折扣后的价格。thenCompose()在异步操作完成的时候对异步操作的结果进行一些操作,Function<? super T, ? extends CompletionStage<U>> fn参数,并且仍然返回CompletableFuture类型,相当于flatMap,用来连接两个CompletableFuture
如果加入一个不相互依赖的Future对象进行整合,比如需要计算汇率(不需要计算折扣,他们之间无依赖),可以试用thenCombine方法,这里有4种实现,最后一种比较优。

public class ExchangeService {

    public enum Money {
        USD(1.0), EUR(1.35387), GBP(1.69715), CAD(.92106), MXN(.07683);

        private final double rate;

        Money(double rate) {
            this.rate = rate;
        }
    }

    public static double getRate(Money source, Money destination) {
        return getRateWithDelay(source, destination);
    }

    private static double getRateWithDelay(Money source, Money destination) {
        delay();
        return destination.rate / source.rate;
    }

}

public List<String> findPricesInUSD(String product) {
        List<CompletableFuture<Double>> priceFutures = new ArrayList<>();
        for (Shop shop : shops) {
            // Start of Listing 10.20.
            // Only the type of futurePriceInUSD has been changed to
            // CompletableFuture so that it is compatible with the
            // CompletableFuture::join operation below.
            CompletableFuture<Double> futurePriceInUSD = 
                CompletableFuture.supplyAsync(() -> shop.getPrice(product))
                .thenCombine(
                    CompletableFuture.supplyAsync(
                        () ->  ExchangeService.getRate(Money.EUR, Money.USD)),
                    (price, rate) -> price * rate
                );
            priceFutures.add(futurePriceInUSD);
        }
        // Drawback: The shop is not accessible anymore outside the loop,
        // so the getName() call below has been commented out.
        List<String> prices = priceFutures
                .stream()
                .map(CompletableFuture::join)
                .map(price -> /*shop.getName() +*/ " price is " + price)
                .collect(Collectors.toList());
        return prices;
    }

    public List<String> findPricesInUSDJava7(String product) {
        ExecutorService executor = Executors.newCachedThreadPool();
        List<Future<Double>> priceFutures = new ArrayList<>();
        for (Shop shop : shops) {
            final Future<Double> futureRate = executor.submit(new Callable<Double>() { 
                public Double call() {
                    return ExchangeService.getRate(Money.EUR, Money.USD);
                }
            });
            Future<Double> futurePriceInUSD = executor.submit(new Callable<Double>() { 
                public Double call() {
                    try {
                        double priceInEUR = shop.getPrice(product);
                        return priceInEUR * futureRate.get();
                    } catch (InterruptedException | ExecutionException e) {
                        throw new RuntimeException(e.getMessage(), e);
                    }
                }
            });
            priceFutures.add(futurePriceInUSD);
        }
        List<String> prices = new ArrayList<>();
        for (Future<Double> priceFuture : priceFutures) {
            try {
                prices.add(/*shop.getName() +*/ " price is " + priceFuture.get());
            }
            catch (ExecutionException | InterruptedException e) {
                e.printStackTrace();
            }
        }
        return prices;
    }

    public List<String> findPricesInUSD2(String product) {
        List<CompletableFuture<String>> priceFutures = new ArrayList<>();
        for (Shop shop : shops) {
            // Here, an extra operation has been added so that the shop name
            // is retrieved within the loop. As a result, we now deal with
            // CompletableFuture<String> instances.
            CompletableFuture<String> futurePriceInUSD = 
                CompletableFuture.supplyAsync(() -> shop.getPrice(product))
                .thenCombine(
                    CompletableFuture.supplyAsync(
                        () -> ExchangeService.getRate(Money.EUR, Money.USD)),
                    (price, rate) -> price * rate
                ).thenApply(price -> shop.getName() + " price is " + price);
            priceFutures.add(futurePriceInUSD);
        }
        List<String> prices = priceFutures
                .stream()
                .map(CompletableFuture::join)
                .collect(Collectors.toList());
        return prices;
    }

    public List<String> findPricesInUSD3(String product) {
        // Here, the for loop has been replaced by a mapping function...
        Stream<CompletableFuture<String>> priceFuturesStream = shops
            .stream()
            .map(shop -> CompletableFuture
                .supplyAsync(() -> shop.getPrice(product))
                .thenCombine(
                    CompletableFuture.supplyAsync(() -> ExchangeService.getRate(Money.EUR, Money.USD)),
                    (price, rate) -> price * rate)
                .thenApply(price -> shop.getName() + " price is " + price));
        // However, we should gather the CompletableFutures into a List so that the asynchronous
        // operations are triggered before being "joined."
        List<CompletableFuture<String>> priceFutures = priceFuturesStream.collect(Collectors.toList());
        List<String> prices = priceFutures
            .stream()
            .map(CompletableFuture::join)
            .collect(Collectors.toList());
        return prices;
    }

 

以上是关于了不起的Java-CompletableFuture组合异步编程的主要内容,如果未能解决你的问题,请参考以下文章

ORM 有啥了不起的?

Func<> 委托有啥了不起的?

读后感读《了不起的盖茨比》后感

了不起的Chrome浏览器:Chrome 97发布WebTransport,QUIC协议小试牛刀

第 007讲:了不起的分支和循环

了不起的Chrome浏览器:Chrome 94开始WebGPU试用,Web的图像渲染及机器学能