CompletableFuture: 组合式异步编程

说明

近这些年,两种趋势不断地推动我们反思我们设计软件的方式。

第一种趋势和应用运行的 硬件平台相关,第二种趋势与应用程序的架构相关,尤其是它们之间如何交互。我们在第7章中 已经讨论过硬件平台的影响。我们注意到随着多核处理器的出现,提升应用程序处理速度有效 的方式是编写能充分发挥多核能力的软件。你已经看到通过切分大型的任务,让每个子任务并行 运行,这一目标是能够实现的;你也已经了解相对直接使用线程的方式,使用分支/合并框架(在 Java 7中引入)和并行流(在Java 8中新引入)能以更简单、更有效的方式实现这一目标。

第二种趋势反映在公共API日益增长的互联网服务应用。著名的互联网大鳄们纷纷提供了自 己的公共API服务,比如谷歌提供了地理信息服务,Facebook提供了社交信息服务,Twitter提供 了新闻服务。现在,很少有网站或者网络应用会以完全隔离的方式工作。更多的时候,我们看到 的下一代网络应用都采用“混聚”(mash-up)的方式:它会使用来自多个来源的内容,将这些内 容聚合在一起,方便用户的生活。 比如,你可能希望为你的法国客户提供指定主题的热点报道。为实现这一功能,你需要向 谷歌或者Twitter的API请求所有语言中针对该主题热门的评论,可能还需要依据你的内部算法 对它们的相关性进行排序。之后,你可能还需要使用谷歌的翻译服务把它们翻译成法语,甚至 利用谷歌地图服务定位出评论作者的位置信息,终将所有这些信息聚集起来,呈现在你的网 站上。 当然,如果某些外部网络服务发生响应慢的情况,你希望依旧能为用户提供部分信息,比如提供带问号标记的通用地图,以文本的方式显示信息,而不是呆呆地显示一片空白屏幕,直到地 图服务器返回结果或者超时退出。

如下图所示:

CompletableFuture: 组合式异步编程_第1张图片

要实现类似的服务,你需要与互联网上的多个Web服务通信。可是,你并不希望因为等待某 些服务的响应,阻塞应用程序的运行,浪费数十亿宝贵的CPU时钟周期。比如,不要因为等待 Facebook的数据,暂停对来自Twitter的数据处理。 这些场景体现了多任务程序设计的另一面。第7章中介绍的分支/合并框架以及并行流是实现 并行处理的宝贵工具;它们将一个操作切分为多个子操作,在多个不同的核、CPU甚至是机器上 并行地执行这些子操作。 与此相反,如果你的意图是实现并发,而非并行,或者你的主要目标是在同一个CPU上执 行几个松耦合的任务,充分利用CPU的核,让其足够忙碌,从而大化程序的吞吐量,那么你 其实真正想做的是避免因为等待远程服务的返回,或者对数据库的查询,而阻塞线程的执行, 浪费宝贵的计算资源,因为这种等待的时间很可能相当长。通过本章中你会了解,Future接口, 尤其是它的新版实现CompletableFuture,是处理这种情况的利器。

并行和并发 的区别

CompletableFuture: 组合式异步编程_第2张图片

1.Future 接口 

Future接口在Java 5中被引入,设计初衷是对将来某个时刻会发生的结果进行建模。它建模 了一种异步计算,返回一个执行运算结果的引用,当运算结束后,这个引用被返回给调用方。在 Future中触发那些潜在耗时的操作把调用线程解放出来,让它能继续执行其他有价值的工作, 不再需要呆呆等待耗时的操作完成。打个比方,你可以把它想象成这样的场景:你拿了一袋子衣 服到你中意的干洗店去洗。干洗店的员工会给你张发票,告诉你什么时候你的衣服会洗好(这就 是一个Future事件)。衣服干洗的同时,你可以去做其他的事情。Future的另一个优点是它比 更底层的Thread更易用。要使用Future,通常你只需要将耗时的操作封装在一个Callable对 象中,再将它提交给ExecutorService,就万事大吉了。

使用Future以异步的方式执行一个耗时的操作 

CompletableFuture: 组合式异步编程_第3张图片

这种编程方式让你的线程可以在ExecutorService以并发方式调 用另一个线程执行耗时操作的同时,去执行一些其他的任务。接着,如果你已经运行到没有异步 操作的结果就无法继续任何有意义的工作时,可以调用它的get方法去获取操作的结果。如果操 作已经完成,该方法会立刻返回操作的结果,否则它会阻塞你的线程,直到操作完成,返回相应 的结果。 你能想象这种场景存在怎样的问题吗?如果该长时间运行的操作永远不返回了会怎样?为 了处理这种可能性,虽然Future提供了一个无需任何参数的get方法,我们还是推荐大家使用重 载版本的get方法,它接受一个超时的参数,通过它,你可以定义你的线程等待Future结果的 长时间,而不是永无止境地等待下去。 

使用Future以异步方式执行长时间的操作 

CompletableFuture: 组合式异步编程_第4张图片

Future 接口的局限性 

通过第一个例子,我们知道Future接口提供了方法来检测异步计算是否已经结束(使用 isDone方法),等待异步操作结束,以及获取计算的结果。但是这些特性还不足以让你编写简洁 的并发代码。比如,我们很难表述Future结果之间的依赖性;从文字描述上这很简单,“当长时 间计算任务完成时,请将该计算的结果通知到另一个长时间运行的计算任务,这两个计算任务都 完成后,将计算的结果与另一个查询操作结果合并”。但是,使用Future中提供的方法完成这样 的操作又是另外一回事。这也是我们需要更具描述能力的特性的原因,比如下面这些。

 将两个异步计算合并为一个——这两个异步计算之间相互独立,同时第二个又依赖于第 一个的结果。

 等待Future集合中的所有任务都完成。

 仅等待Future集合中快结束的任务完成(有可能因为它们试图通过不同的方式计算同 一个值),并返回它的结果。

 通过编程方式完成一个Future任务的执行(即以手工设定异步操作结果的方式)。

 应对Future的完成事件(即当Future的完成事件发生时会收到通知,并能使用Future 计算的结果进行下一步的操作,不只是简单地阻塞等待操作的结果)。

在此,你会了解新的CompletableFuture类(它实现了Future接口)如何利用Java 8 的新特性以更直观的方式将上述需求都变为可能。Stream和CompletableFuture的设计都遵循 了类似的模式:它们都使用了Lambda表达式以及流水线的思想。从这个角度,你可以说 CompletableFuture和Future的关系就跟Stream和Collection的关系一样。 

使用CompletableFuture 构建异步应用 

为了展示CompletableFuture的强大特性,我们会创建一个名为“佳价格查询器” (best-price-finder)的应用,它会查询多个在线商店,依据给定的产品或服务找出低的价格。这个过程中,你会学到几个重要的技能。

 首先,你会学到如何为你的客户提供异步API(如果你拥有一间在线商店的话,这是非常 有帮助的)。

 其次,你会掌握如何让你使用了同步API的代码变为非阻塞代码。你会了解如何使用流水 线将两个接续的异步操作合并为一个      异步计算操作。这种情况肯定会出现,比如,在线 商店返回了你想要购买商品的原始价格,并附带着一个折扣代码——终,        要 计算出该 商品的实际价格,你不得不访问第二个远程折扣服务,查询该折扣代码对应的折扣比率。
  你还会学到如何以响应式的方式处理异步操作的完成事件,以及随着各个商店返回它的 商品价格,佳价格查询器如何持续地         更新每种商品的佳推荐,而不是等待所有的商 店都返回他们各自的价格(这种方式存在着一定的风险,一旦某家商店的服务       中断,用 户可能遭遇白屏)。

 同步API与异步API 同步API其实只是对传统方法调用的另一种称呼:你调用了某个方法,调用方在被调用方 运行的过程中会等待,被调用方运行结束返回,调用方取得被调用方的返回值并继续运行。即 使调用方和被调用方在不同的线程中运行,调用方还是需要等待被调用方结束运行,这就是阻 塞式调用这个名词的由来。 与此相反,异步API会直接返回,或者至少在被调用方计算完成之前,将它剩余的计算任 务交给另一个线程去做,该线程和调用方是异步的——这就是非阻塞式调用的由来。执行剩余 计算任务的线程会将它的计算结果返回给调用方。返回的方式要么是通过回调函数,要么是由 调用方再次执行一个“等待,直到计算完成”的方法调用。这种方式的计算在I/O系统程序设 计中非常常见:你发起了一次磁盘访问,这次访问和你的其他计算操作是异步的,你完成其他 的任务时,磁盘块的数据可能还没载入到内存,你只需要等待数据的载入完成。 

实现异步API 

     //模拟 1秒钟延迟的方法
    public void delay() {
        try {
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    //在getPrice方法中引入一个模拟的延迟
    public double getPrice(String product) {
        return calculatePrice(product);
    }

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

很明显,这个API的使用者(这个例子中为佳价格查询器)调用该方法时,它依旧会被 阻塞。为等待同步事件完成而等待1秒钟,这是无法接受的,尤其是考虑到佳价格查询器对 网络中的所有商店都要重复这种操作。

将同步方法转换为异步方法 

    //异步调用
    public Future getPriceAsync(String product) {
        //创建CompletableFuture 对象,它会包含计算的结果
        CompletableFuture futurePrice = new CompletableFuture<>();
        //在另一个 线程中以 异步方式 执行计算
        new Thread(()->{
            double price = calculatePrice(product);
            //需长时间计算的任务结 束并得出结果时,设置 Future的返回值
            futurePrice.complete(price);
        }).start();
        //无需等待还没结束的计 算,直接返回Future对象
        return futurePrice;
    }

在这段代码中,你创建了一个代表异步计算的CompletableFuture对象实例,它在计算完 成时会包含计算的结果。接着,你调用fork创建了另一个线程去执行实际的价格计算工作,不 等该耗时计算任务结束,直接返回一个Future实例。当请求的产品价格终计算得出时,你可 以使用它的complete方法,结束completableFuture对象的运行,并设置变量的值。很显然, 这个新版Future的名称也解释了它所具有的特性。使用这个API的客户端,可以通过下面的这段 代码对其进行调用。

    @Test
    public void shop1() {
        Shop shop = new Shop();
        long start = System.nanoTime();
        //查询商店,试图 取得商品的价格
        Future favoriteProduct = shop.getPriceAsync("my favorite product");
        long invocationTime = ((System.nanoTime() - start) / 1_000_000);
        System.out.println("Invocation returned after " + invocationTime  + " msecs");
        // 执行更多任务,比如查询其他商店
        doSomethingElse();
        // 在计算商品价格的同时
        try {
            double price = favoriteProduct.get();
            System.out.printf("Price is %.2f%n", price);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        long retrievalTime = ((System.nanoTime() - start) / 1_000_000);
        System.out.println("Price returned after " + retrievalTime + " msecs");
    }

    private void doSomethingElse() {
        System.out.println("其他商店");
    }

我们看到这段代码中,客户向商店查询了某种商品的价格。由于商店提供了异步API,该次 调用立刻返回了一个Future对象,通过该对象客户可以在将来的某个时刻取得商品的价格。这 种方式下,客户在进行商品价格查询的同时,还能执行一些其他的任务,比如查询其他家商店中 商品的价格,不会呆呆地阻塞在那里等待第一家商店返回请求的结果。后,如果所有有意义的 工作都已经完成,客户所有要执行的工作都依赖于商品价格时,再调用Future的get方法。执行 了这个操作后,客户要么获得Future中封装的值(如果异步任务已经完成),要么发生阻塞,直 到该异步任务完成,期望的值能够访问。代码输出:

nvocation returned after 131 msecs
其他商店
Price is 214.12
Price returned after 1141 msecs

 错误处理 

如果没有意外,我们目前开发的代码工作得很正常。但是,如果价格计算过程中产生了错误 会怎样呢?非常不幸,这种情况下你会得到一个相当糟糕的结果:用于提示错误的异常会被限制 在试图计算商品价格的当前线程的范围内,终会杀死该线程,而这会导致等待get方法返回结 果的客户端永久地被阻塞。 客户端可以使用重载版本的get方法,它使用一个超时参数来避免发生这样的情况。这是一 种值得推荐的做法,你应该尽量在你的代码中添加超时判断的逻辑,避免发生类似的问题。使用 这种方法至少能防止程序永久地等待下去,超时发生时,程序会得到通知发生了Timeout- Exception。不过,也因为如此,你不会有机会发现计算商品价格的线程内到底发生了什么问题 才引发了这样的失效。为了让客户端能了解商店无法提供请求商品价格的原因,你需要使用 CompletableFuture的completeExceptionally方法将导致CompletableFuture内发生问 题的异常抛出。代码优化如下:

    //异步调用
    public Future getPriceAsync(String product) {
        //创建CompletableFuture 对象,它会包含计算的结果
        CompletableFuture futurePrice = new CompletableFuture<>();
        //在另一个 线程中以 异步方式 执行计算
        new Thread(()->{
            try {
                double price = calculatePrice(product);
                //需长时间计算的任务结 束并得出结果时,设置 Future的返回值
                futurePrice.complete(price);
            } catch (Exception e) {
                //获取异常
                futurePrice.completeExceptionally(e);
            }
        }).start();
        //无需等待还没结束的计 算,直接返回Future对象
        return futurePrice;
    }

客户端现在会收到一个ExecutionException异常,该异常接收了一个包含失败原因的 Exception参数,即价格计算方法初抛出的异常。

使用工厂方法supplyAsync创建CompletableFuture 

目前为止我们已经了解了如何通过编程创建CompletableFuture对象以及如何获取返回 值,虽然看起来这些操作已经比较方便,但还有进一步提升的空间,CompletableFuture类自 身提供了大量精巧的工厂方法,使用这些方法能更容易地完成整个流程,还不用担心实现的细节。 比如,采用supplyAsync方法后,你可以用一行语句重写getPriceAsync方 法,如下所示。 

 

supplyAsync方法接受一个生产者(Supplier)作为参数,返回一个CompletableFuture 对象,该对象完成异步执行后会读取调用生产者方法的返回值。生产者方法会交由ForkJoinPool 池中的某个执行线程(Executor)运行,但是你也可以使用supplyAsync方法的重载版本,传 递第二个参数指定不同的执行线程执行生产者方法。一般而言,向CompletableFuture的工厂 方法传递可选参数,指定生产者方法的执行线程是可行的。

此外,之前getPriceAsync方法返回的CompletableFuture对象和此时完成的CompletableFuture对象是完全等价的,这意味着它提供了同样的 错误管理机制,而前者你花费了大量的精力才得以构建。 

让你的代码免受阻塞之苦 

所以,你已经被要求进行“佳价格查询器”应用的开发了,不过你需要查询的所有商 店都如开始时介绍的那样,只提供了同步API。换句话说,你有一个商家的列表,你需要使用下面这样的签名实现一个方法,它接受产品名作为参数,返回一个字符串列表, 这个字符串列表中包括商店的名称、该商店中指定商品的价格:

    //价格查询器
    public List findPrices(String product) {
        List shops = Arrays.asList(new Shop("2元店"),
                new Shop("水果连锁店"),
                new Shop("超市"),
                new Shop("常去小卖铺"),
                new Shop("商场"));
        return shops.stream()
                .map(shop -> String.format("%s price is %.2f",shop.getName(),shop.getPrice(product)))
                .collect(Collectors.toList());
    }

执行验证:

    @Test
    public void shop2() {
        //纳秒
        long start = System.nanoTime();
        System.out.println(findPrices("iphone xs plus"));
        long duration = (System.nanoTime() - start) / 1_000_000;
        System.out.println("Done in " + duration + " msecs");
    }

//结果
[廉价店 price is 192.05, 水果连锁店 price is 148.06, 超市 price is 162.45, 常去小卖铺 price is 191.72, 商场 price is 144.58]
Done in 5097 msecs

因为对这5个商店 的查询是顺序进行的,并且一个查询操作会阻塞另一个,每一个操作都要花费大约1秒左右的时 间计算请求商品的价格。你怎样才能改进这个结果呢? 

使用并行流对请求进行并行操作 

    //价格查询器
    public List findPrices(String product) {
        List shops = Arrays.asList(new Shop("廉价店"),
                new Shop("水果连锁店"),
                new Shop("超市"),
                new Shop("常去小卖铺"),
                new Shop("商场"));
        return shops.stream().parallel()//并行流
                .map(shop -> String.format("%s price is %.2f",shop.getName(),shop.getPrice(product)))
                .collect(Collectors.toList());
    }

//结果
[廉价店 price is 165.32, 水果连锁店 price is 173.01, 超市 price is 149.70, 常去小卖铺 price is 124.12, 商场 price is 124.11]
Done in 2127 msecs

 

使用CompletableFuture 发起异步请求 

    //价格查询器
    public List findPrices2(String product) {
        List shops = Arrays.asList(new Shop("廉价店"),
                new Shop("水果连锁店"),
                new Shop("超市"),
                new Shop("常去小卖铺"),
                new Shop("商场"));
        List> futures = shops.stream()
                .map(shop -> CompletableFuture.supplyAsync(() -> String.format("%s price is %.2f", shop.getName(), shop.getPrice(product))))
                .collect(Collectors.toList());
        return futures.stream()
                .map(CompletableFuture::join)
                .collect(Collectors.toList());
    }

//结果
[廉价店 price is 198.01, 水果连锁店 price is 134.25, 超市 price is 139.24, 常去小卖铺 price is 208.96, 商场 price is 197.33]
Done in 2090 msecs

CompletableFuture类中的join方法和Future接口中的get有相同的含义,并且也声明在 Future接口中,它们唯一的不同是join不会抛出任何检测到的异常。使用它你不再需要使用 try/catch语句块让你传递给第二个map方法的Lambda表达式变得过于臃肿。

注意到了吗?这里使用了两个不同的Stream流水线,而不是在同一个处理流的流水线上一 个接一个地放置两个map操作——这其实是有缘由的。考虑流操作之间的延迟特性,如果你在单 一流水线中处理流,发向不同商家的请求只能以同步、顺序执行的方式才会成功。因此,每个创 建CompletableFuture对象只能在前一个操作结束之后执行查询指定商家的动作、通知join 方法返回计算结果。

CompletableFuture: 组合式异步编程_第5张图片

上半部分展示了使用单一流水线处理流的过程,我们看到,执行的流程(以虚线标 识)是顺序的。事实上,新的CompletableFuture对象只有在前一个操作完全结束之后,才能 创建。与此相反,图的下半部分展示了如何先将CompletableFutures对象聚集到一个列表中 (即图中以椭圆表示的部分),让对象们可以在等待其他对象完成操作之前就能启动。 

这个结果让人相当失望,不是吗?超过2秒意味着利用CompletableFuture实现的版本, 比刚开始代码清单11-8中原生顺序执行且会发生阻塞的版本快。但是它的用时也差不多是使用并 行流的前一个版本的两倍。尤其是,考虑到从顺序执行的版本转换到并行流的版本只做了非常小 的改动,就让人更加沮丧。 

与此形成鲜明对比的是,我们为采用CompletableFutures完成的新版方法做了大量的工 作!但,这就是全部的真相吗?这种场景下使用CompletableFutures真的是浪费时间吗?或 者我们可能漏掉了某些重要的东西?继续往下探究之前,让我们休息几分钟,尤其是想想你测试 代码的机器是否足以以并行方式运行四个线程。

寻找更好的方案 

并行流的版本工作得非常好,那是因为它能并行地执行四个任务,所以它几乎能为每个商家 分配一个线程。但是,如果你想要增加第五个商家到商店列表中,让你的“佳价格查询”应用 对其进行处理,这时会发生什么情况?毫不意外,顺序执行版本的执行还是需要大约5秒多钟的 时间,非常不幸,并行流版本的程序这次比之前也多消耗了差不多1秒钟的时间,因为可以并行运 行(通用线程池中处于可用状态的)的四个线程现在都处于繁忙状态,都在对前4个商店进行查 询。第五个查询只能等到前面某一个操作完成释放出空闲线程才能继续。

CompletableFuture版本的程序似乎比并行流版本的程序还快那么一点儿。它们看起来不相伯仲,究其原因都一样:它们内部 采用的是同样的通用线程池,默认都使用固定数目的线程,具体线程数取决于Runtime. getRuntime().availableProcessors()的返回值。然而,CompletableFuture具有一定的 优势,因为它允许你对执行器(Executor)进行配置,尤其是线程池的大小,让它以更适合应用需求的方式进行配置,满足程序的要求,而这是并行流API无法提供的。让我们看看你怎样利 用这种配置上的灵活性带来实际应用程序性能上的提升。 

使用定制的执行器 

CompletableFuture: 组合式异步编程_第6张图片

你的应用99%的时间都在等待商店的响应,所以估算出的W/C比率为100。这意味着如果你 期望的CPU利用率是100%,你需要创建一个拥有400个线程的线程池。实际操作中,如果你创建 的线程数比商店的数目更多,反而是一种浪费,因为这样做之后,你线程池中的有些线程根本没 有机会被使用。出于这种考虑,我们建议你将执行器使用的线程数,与你需要查询的商店数目设 定为同一个值,这样每个商店都应该对应一个服务线程。不过,为了避免发生由于商店的数目过 多导致服务器超负荷而崩溃,你还是需要设置一个上限,比如100个线程。

修改代码如下:

    //价格查询器
    public List findPrices2(String product) {
        List> futures = shops.stream()
                .map(shop -> CompletableFuture.supplyAsync(() ->
                        String.format("%s price is %.2f", shop.getName(), shop.getPrice(product)),executor))
                .collect(Collectors.toList());
        return futures.stream()
                .map(CompletableFuture::join)
                .collect(Collectors.toList());
    }

    //创建一个线 程池,线程 池中线程的 数目为100 和商店数目 二者中较小 的一个值
    private final Executor executor = Executors.newFixedThreadPool(Math.min(shops.size(), 100), new ThreadFactory() {
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r);
            //使用守护线程——这 种方式不会阻止程序 的关停
            t.setDaemon(true);
            return t;
        }
    });

//结果
[廉价店 price is 147.19, 水果连锁店 price is 166.02, 超市 price is 189.18, 常去小卖铺 price is 173.72, 商场 price is 209.23]
Done in 1098 msecs

 

并行——使用流还是CompletableFutures?

目前为止,你已经知道对集合进行并行计算有两种方式:要么将其转化为并行流,利用map 这样的操作开展工作,要么枚举出集合中的每一个元素,创建新的线程,在Completable- Future内对其进行操作。后者提供了更多的灵活性,你可以调整线程池的大小,而这能帮助 你确保整体的计算不会因为线程都在等待I/O而发生阻塞。 我们对使用这些API的建议如下。

❑如果你进行的是计算密集型的操作,并且没有I/O,那么推荐使用Stream接口,因为实 现简单,同时效率也可能是最高的(如果所有的线程都是计算密集型的,那就没有必要 创建比处理器核数更多的线程)。

❑反之,如果你并行的工作单元还涉及等待I/O的操作(包括网络连接等待),那么使用 CompletableFuture灵活性更好,你可以像前文讨论的那样,依据等待/计算,或者 W/C的比率设定需要使用的线程数。这种情况不使用并行流的另一个原因是,处理流的 流水线中如果发生I/O等待,流的延迟特性会让我们很难判断到底什么时候触发了等待。
 

对多个异步任务进行流水线操作 

让我们假设所有的商店都同意使用一个集中式的折扣服务。该折扣服务提供了五个不同的折 扣代码,每个折扣代码对应不同的折扣率。具体代码如下所示。 

/**
 * 折扣类
 */
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) {
        Shop shop = new Shop();
        //模拟Discount 服务响应的延迟
        shop.delay();
        return price * (100 - code.percentage) / 100;
    }
}


    //在getPrice方法中引入一个模拟的延迟
    public String getPrice2(String product) {
        double price = calculatePrice(product);
        Discount.Code code = Discount.Code.values()[random.nextInt(Discount.Code.values().length)];
        return String.format("%s:%.2f:%s", product, price, code);
    }



 /**
 * 对商店返回字符串的解析操作封装到此类
 * 通过传递shop对象返回的字符串给静态工厂方法parse,你可以得到Quote类的一个实例,
 * 它包含了shop的名称、折扣之前的价格,以及折扣代码
 */
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;
    }
}

//顺序执行
public List findPrices3(String product) {
        return shops.stream()
                //取得每个shop对象 中商品的原始价格
                .map(shop -> shop.getPrice2(product))
                //在Quote对象中 对shop返回的字 符串进行转换
                .map(Quote::parse)
                //联系Discount服 务,为每个Quote 申请折扣
                .map(Discount::applyDiscount)
                .collect(Collectors.toList());
    }

    @Test
    public void shop5() {
        //纳秒
        long start = System.nanoTime();
        System.out.println(findPrices3("iphone xs plus"));
        long duration = (System.nanoTime() - start) / 1_000_000;
        System.out.println("Done in " + duration + " msecs");
    }


//结果
[iphone xs plus price is 158.954, iphone xs plus price is 93.904, iphone xs plus price is 147.0885, iphone xs plus price is 169.576, iphone xs plus price is 113.37]
Done in 10128 msecs

//开启并行流的结果
[iphone xs plus price is 145.408, iphone xs plus price is 117.24799999999999, iphone xs plus price is 105.993, iphone xs plus price is 196.7925, iphone xs plus price is 165.642]
Done in 4193 msecs

 

通过在shop构成的流上采用流水线方式执行三次map操作,我们得到了期望的结果。

 第一个操作将每个shop对象转换成了一个字符串,该字符串包含了该 shop中指定商品的 价格和折扣代码。

 第二个操作对这些字符串进行了解析,在Quote对象中对它们进行转换。

 终,第三个map会操作联系远程的Discount服务,计算出终的折扣价格,并返回该 价格及提供该价格商品的shop。

你可能已经猜到,这种实现方式的性能远非优,这次执行耗时10秒,因为顺序查询5个商店耗时大约5秒,现在又加上了Discount 服务为5个商店返回的价格申请折扣所消耗的5秒钟。你已经知道,把流转换为并行流的方式,非 常容易提升该程序的性能。不过,通过之前的介绍,你也知道这一方案在商店的数目增加时, 扩展性不好,因为Stream底层依赖的是线程数量固定的通用线程池。相反,你也知道,如果自 定义CompletableFutures调度任务执行的执行器能够更充分地利用CPU资源。

 构造同步和异步操作 

改造findPrices3方法如下:

public List findPrices4(String product) {
        List> futures = shops.stream()
                //.parallel()
                //取得每个shop对象 中商品的原始价格
                .map(shop -> CompletableFuture.supplyAsync(() -> shop.getPrice2(product), executor))
                //在Quote对象中 对shop返回的字 符串进行转换
                .map(future -> future.thenApply(Quote::parse))
                //联系Discount服 务,为每个Quote 申请折扣
                .map(future -> future.thenCompose(quote ->
                        CompletableFuture.supplyAsync(() -> Discount.applyDiscount(quote), executor)))
                .collect(Collectors.toList());
        return futures.stream()
                .map(CompletableFuture::join)
                .collect(Collectors.toList());
    }


//结果
[iphone xs plus price is 186.96, iphone xs plus price is 153.9, iphone xs plus price is 184.61, iphone xs plus price is 188.24, iphone xs plus price is 153.10199999999998]
Done in 2164 msecs

构造同步操作和异步任务

CompletableFuture: 组合式异步编程_第7张图片

1. 获取价格

这三个操作中的第一个你已经在本章的各个例子中见过很多次,只需要将Lambda表达式作 为参数传递给supplyAsync工厂方法就可以以异步方式对shop进行查询。第一个转换的结果是 一个Stream>,一旦运行结束,每个CompletableFuture对 象中都会包含对应shop返回的字符串。注意,你对CompletableFuture进行了设置,向其传递了一个订制的执行器Executor。

2. 解析报价

现在你需要进行第二次转换将字符串转变为订单。由于一般情况下解析操作不涉及任何远程 服务,也不会进行任何I/O操作,它几乎可以在第一时间进行,所以能够采用同步操作,不会带 来太多的延迟。由于这个原因,你可以对第一步中生成的CompletableFuture对象调用它的 thenApply,将一个由字符串转换Quote的方法作为参数传递给它。 注意到了吗?直到你调用的CompletableFuture执行结束,使用的thenApply方法都不会 阻塞你代码的执行。这意味着CompletableFuture终结束运行时,你希望传递Lambda表达式 给thenApply方法,将Stream中的每个CompletableFuture对象转换为对应的 CompletableFuture对象。你可以把这看成是为处理CompletableFuture的结果建立了一个菜单,就像你曾经为Stream的流水线所做的事儿一样。 

3. 为计算折扣价格构造Future

第三个map操作涉及联系远程的Discount服务,为从商店中得到的原始价格申请折扣率。 这一转换与前一个转换又不大一样,因为这一转换需要远程执行(或者,就这个例子而言,它需 要模拟远程调用带来的延迟),出于这一原因,你也希望它能够异步执行。 为了实现这一目标,你像第一个调用传递getPrice给supplyAsync那样,将这一操作以 Lambda表达式的方式传递给了supplyAsync工厂方法,该方法终会返回另一个Completable- Future对象。到目前为止,你已经进行了两次异步操作,用了两个不同的CompletableFutures 对象进行建模,你希望能把它们以级联的方式串接起来进行工作。

 从shop对象中获取价格,接着把价格转换为Quote。

 拿到返回的Quote对象,将其作为参数传递给Discount服务,取得终的折扣价格。

Java 8的 CompletableFuture API提供了名为thenCompose的方法,它就是专门为这一目 的而设计的,thenCompose方法允许你对两个异步操作进行流水线,第一个操作完成时,将其 结果作为参数传递给第二个操作。换句话说,你可以创建两个CompletableFutures对象,对 第一个CompletableFuture对象调用thenCompose,并向其传递一个函数。当第一个 CompletableFuture执行完毕后,它的结果将作为该函数的参数,这个函数的返回值是以第一 个CompletableFuture的返回做输入计算出的第二个CompletableFuture对象。使用这种方 式,即使Future在向不同的商店收集报价,主线程还是能继续执行其他重要的操作,比如响应 UI事件。 将这三次map操作的返回的Stream元素收集到一个列表,你就得到了一个List>,等这些CompletableFuture对象终执行完毕,你就可以像代码中那样利用join取得它们的返回值。

使用的thenCompose方法像CompletableFuture类中的其他方法一 样,也提供了一个以Async后缀结尾的版本thenComposeAsync。通常而言,名称中不带Async 的方法和它的前一个任务一样,在同一个线程中运行;而名称以Async结尾的方法会将后续的任 务提交到一个线程池,所以每个任务是由不同的线程处理的。就这个例子而言,第二个 CompletableFuture对象的结果取决于第一个CompletableFuture,所以无论你使用哪个版 本的方法来处理CompletableFuture对象,对于终的结果,或者大致的时间而言都没有多少 差别。我们选择thenCompose方法的原因是因为它更高效一些,因为少了很多线程切换的开销。

将两个 CompletableFuture 对象整合起来,无论它们是否存在依赖 

你对一个CompletableFuture对象调用了thenCompose方法,并向其传递了第二个CompletableFuture,而第二个CompletableFuture又需要使用第一个 CompletableFuture的执行结果作为输入。但是,另一种比较常见的情况是,你需要将两个完 全不相干的CompletableFuture对象的结果整合起来,而且你也不希望等到第一个任务完全结 束才开始第二项任务。 这种情况,你应该使用thenCombine方法,它接收名为BiFunction的第二参数,这个参数 定义了当两个CompletableFuture对象完成计算后,结果如何合并。同thenCompose方法一样, thenCombine方法也提供有一个Async的版本。这里,如果使用thenCombineAsync会导致 BiFunction中定义的合并操作被提交到线程池中,由另一个任务以异步的方式执行。 
 回到我们正在运行的这个例子,你知道,有一家商店提供的价格是以欧元(EUR)计价的, 但是你希望以美元的方式提供给你的客户。你可以用异步的方式向商店查询指定商品的价格,同 时从远程的汇率服务那里查到欧元和美元之间的汇率。当二者都结束时,再将这两个结果结合起 来,用返回的商品价格乘以当时的汇率,得到以美元计价的商品价格。用这种方式,你需要使用 第三个CompletableFuture 对象,当前两个CompletableFuture 计算出结果,并由 BiFunction方法完成合并后,由它来最终结束这一任务,代码清单如下所示。 

合并两个独立的CompletableFuture对象 

CompletableFuture: 组合式异步编程_第8张图片

这里整合的操作只是简单的乘法操作,用另一个单独的任务对其进行操作有些浪费资源,所 以你只要使用thenCombine方法,无需特别求助于异步版本的thenCombineAsync方法。如下图展示了代码中创建的多个任务是如何在线程池中选择不同的线程执行的,以及它们最 终的运行结果又是如何整合的。 

CompletableFuture: 组合式异步编程_第9张图片

响应CompletableFuture 的 completion 事件 

本文你看到的所有示例代码都是通过在响应之前添加1秒钟的等待延迟模拟方法的远程调 用。毫无疑问,现实世界中,你的应用访问各个远程服务时很可能遭遇无法预知的延迟,触发的 原因多种多样,从服务器的负荷到网络的延迟,有些甚至是源于远程服务如何评估你应用的商业 价值,即可能相对于其他的应用,你的应用每次查询的消耗时间更长。 由于这些原因,你希望购买的商品在某些商店的查询速度要比另一些商店更快。为了说明本次的内容,我们以下面的代码清单为例,使用randomDelay方法取代原来的固定延迟。 

private static final Random random = new Random();
    //一个模拟生成0.5秒至2.5秒随机延迟的方法 
    public static void randomDelay() {
        int delay = 500 + random.nextInt(2000);
        try {
            Thread.sleep(delay);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

目前为止,你实现的findPrices方法只有在取得所有商店的返回值时才显示商品的价格。 而你希望的效果是,只要有商店返回商品价格就在第一时间显示返回值,不再等待那些还未返回 的商店(有些甚至会发生超时)。你如何实现这种更进一步的改进要求呢? 

对最佳价格查询器应用的优化 

你要避免的首要问题是,等待创建一个包含了所有价格的List创建完成。你应该做的是直 接处理CompletableFuture流,这样每个CompletableFuture都在为某个商店执行必要的操 作。为了实现这一目标,在下面的代码清单中,实现findPricesStream方法来生成一个由CompletableFuture构成的流。 

public Stream> findPricesStream(String product) {
        return shops.stream()
                .map(shop -> CompletableFuture.supplyAsync(() -> shop.getPrice2(shop.getName()), executor))
                .map(future -> future.thenApply(Quote::parse))
                .map(future -> future.thenCompose(quote ->
                        CompletableFuture.supplyAsync(() -> Discount.applyDiscount(quote), executor)));
    }

    @Test
    public void shop6() {
        //纳秒
        long start = System.nanoTime();
        CompletableFuture[] iphoneXsPluses = findPricesStream("iphone xs plus")
                .map(future -> future.thenAccept(System.out::println))
                .toArray(CompletableFuture[]::new);
        CompletableFuture.anyOf(iphoneXsPluses).join();
        //CompletableFuture.allOf(iphoneXsPluses).join();
        long duration = (System.nanoTime() - start) / 1_000_000;
        System.out.println("Done in " + duration + " msecs");
    }

 

现在,你为findPricesStream方法返回的Stream添加了第四个map操作,在此之前,你 已经在该方法内部调用了三次map。这个新添加的操作其实很简单,只是在每个 CompletableFuture上注册一个操作,该操作会在CompletableFuture完成执行后使用它的 返回值。Java 8的CompletableFuture通过thenAccept方法提供了这一功能,它接收 CompletableFuture执行完毕后的返回值做参数。在这里的例子中,该值是由Discount服务 返回的字符串值,它包含了提供请求商品的商店名称及折扣价格,你想要做的操作也很简单,只 是将结果打印输出。

注意,和你之前看到的thenCompose和thenCombine方法一样,thenAccept方法也提供 了一个异步版本,名为thenAcceptAsync。异步版本的方法会对处理结果的消费者进行调度, 从线程池中选择一个新的线程继续执行,不再由同一个线程完成CompletableFuture的所有任 务。因为你想要避免不必要的上下文切换,更重要的是你希望避免在等待线程上浪费时间,尽快 响应CompletableFuture的completion事件,所以这里没有采用异步版本。 

由于thenAccept方法已经定义了如何处理CompletableFuture返回的结果,一旦 CompletableFuture计算得到结果,它就返回一个CompletableFuture。所以,map 操作返回的是一个Stream>。对这个>对象,你能做的事非常有限,只能等待其运行结束,不过这也是你所期望的。你还希望 能给慢的商店一些机会,让它有机会打印输出返回的价格。为了实现这一目的,你可以把构成 Stream的所有CompletableFuture对象放到一个数组中,等待所有的任务执行完成。

 allOf工厂方法接收一个由CompletableFuture构成的数组,数组中的所有Completable- Future对象执行完成之后,它返回一个CompletableFuture对象。这意味着,如果你需 要等待初Stream中的所有CompletableFuture对象执行完毕,对allOf方法返回的 CompletableFuture执行join操作是个不错的主意。这个方法对“佳价格查询器”应用也是 有用的,因为你的用户可能会困惑是否后面还有一些价格没有返回,使用这个方法,你可以在执 行完毕之后打印输出一条消息“All shops returned results or timed out”。 然而在另一些场景中,你可能希望只要CompletableFuture对象数组中有任何一个执行完 毕就不再等待,比如,你正在查询两个汇率服务器,任何一个返回了结果都能满足你的需求。在 这种情况下,你可以使用一个类似的工厂方法anyOf。该方法接收一个CompletableFuture对象 构成的数组,返回由第一个执行完毕的CompletableFuture对象的返回值构成的Completable- Future。 

小结


  执行比较耗时的操作时,尤其是那些依赖一个或多个远程服务的操作,使用异步任务可 以改善程序的性能,加快程序的响应速度。

 你应该尽可能地为客户提供异步API。使用CompletableFuture类提供的特性,你能够 轻松地实现这一目标。

 CompletableFuture类还提供了异常管理的机制,让你有机会抛出/管理异步任务执行 中发生的异常。

 将同步API的调用封装到一个CompletableFuture中,你能够以异步的方式使用其结果。
  如果异步任务之间相互独立,或者它们之间某一些的结果是另一些的输入,你可以将这 些异步任务构造或者合并成一个。

 你可以为CompletableFuture注册一个回调函数,在Future执行完毕或者它们计算的 结果可用时,针对性地执行一些程序。

 你可以决定在什么时候结束程序的运行,是等待由CompletableFuture对象构成的列表 中所有的对象都执行完毕,还是只要其中任何一个首先完成就中止程序的运行。 

 

本文理论源自 java8实战

你可能感兴趣的:(java8)