• 了不起的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;
        }
  • 相关阅读:
    mysql配置图解(mysql 5.5)
    C++中的enum
    vc6.0中的dsp,dsw,ncb,opt,clw,plg,aps等文件的简单说明
    using namespace std
    C#中Cache的使用 迎客
    数据库里的存储过程和事务有什么区别? 迎客
    WINDOWS远程默认端口3389的正确修改方式 迎客
    DES加密和解密PHP,Java,ObjectC统一的方法 迎客
    转:15点 老外聊iPhone游戏开发注意事项 迎客
    windows server 2003 删除默认共享 迎客
  • 原文地址:https://www.cnblogs.com/starcrm/p/12427361.html
Copyright © 2020-2023  润新知