探索Java8——CompletableFuture: 组合式异步编程

2023-10-31


如果你的意图是实现并发,而非并行,或者你的主要目标是在同一个CPU上执行几个松耦合的任务,充分利用CPU的核,让其足够忙碌,从而最大化程序的吞吐量,那么你其实真正想做的是 避免因为等待远程服务的返回,或者对数据库的查询,而阻塞线程的执行,浪费宝贵的计算资源,因为这种等待的时间很可能相当长。通过本章中你会了解,Future接口,尤其是它的新版实现CompletableFuture,是处理这种情况的利器。

在这里插入图片描述

Future接口

Future接口在Java 5中被引入,设计初衷是对将来某个时刻会发生的结果进行建模。它建模了一种异步计算,返回一个执行运算结果的引用,当运算结束后,这个引用被返回给调用方。

打个比方,你可以把它想象成这样的场景:你拿了一袋子衣服到你中意的干洗店去洗。干洗店的员工会给你张发票,告诉你什么时候你的衣服会洗好(这就是一个Future事件)。衣服干洗的同时,你可以去做其他的事情。

Future的另一个优点是它比更底层的Thread更易用。要使用Future,通常你只需要将耗时的操作封装在一个Callable对象中,再将它提交给ExecutorService,就万事大吉了。

ExecutorService executor = Executors.newCachedThreadPool(); 
//或者直接用Future<Double> doubleFuture = executor.submit(FutureLambda::doSomeLongComputation);
Future<Double> future = executor.submit(new Callable<Double>() { 
 	public Double call() {
 		//以异步方式在新的线程中执行耗时的操作
 		return doSomeLongComputation(); 
 	}}); 
...
//异步操作进行的同时你可以做其他的事情
doSomethingElse(); 
...
try { 
 	Double result = future.get(1, TimeUnit.SECONDS); 
} catch (ExecutionException ee) {
 	// 计算抛出一个异常
} catch (InterruptedException ie) { 
 	// 当前线程在等待过程中被中断
} catch (TimeoutException te) { 
 	// 在Future对象完成之前超过已过期
} 

Future接口的局限性

Future接口提供了方法来检测异步计算是否已经结束(使用isDone方法),等待异步操作结束,以及获取计算的结果。

但是Future对于依赖性的操作很难描述。比如,将两个异步计算合并为一个——这两个异步计算之间相互独立,同时第二个又依赖于第一个的结果。

使用 CompletableFuture

CompletableFuture方法请参考CompletableFuture 使用详解

你已经被要求进行“最佳价格查询器”应用的开发了,你有一个商家的列表,如下所示:

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

你需要使用下面这样的签名实现一个方法,它接受产品名作为参数,返回一个字符串列表,这个字符串列表中包括商店的名称、该商店中指定商品的价格:
public List<String> findPrices(String product);

利用Stream特性,你可能写出这样的代码:

public List<String> findPrices(String product) { 
 	return shops.stream() 
 		.map(shop -> String.format("%s price is %.2f", 
 						shop.getName(), shop.getPrice(product))) 
 		.collect(toList()); 
} 

验证findPrices的正确性和执行性能:

long start = System.nanoTime(); 
System.out.println(findPrices("myPhone27S")); 
long duration = (System.nanoTime() - start) / 1_000_000; 
System.out.println("Done in " + duration + " msecs"); 

输出结果:
[BestPrice price is 123.26, LetsSaveBig price is 169.47, MyFavoriteShop price 
 is 214.13, BuyItAll price is 184.74] 
Done in 4032 msecs 

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

是最快的改善方法是使用并行流来避免顺序计算,如下所示。

public List<String> findPrices(String product) { 
 	return shops.ParallelStream() 
 		.map(shop -> String.format("%s price is %.2f", 
 						shop.getName(), shop.getPrice(product))) 
 		.collect(toList()); 
} 

运行结果:
[BestPrice price is 123.26, LetsSaveBig price is 169.47, MyFavoriteShop price 
 is 214.13, BuyItAll price is 184.74] 
Done in 1180 msecs 

使用 CompletableFuture 发起异步请求

List<CompletableFuture<String>> priceFutures = 
 	shops.stream() 
 	.map(shop -> CompletableFuture.supplyAsync( 
 		() -> String.format("%s price is %.2f", 
 		shop.getName(), shop.getPrice(product)))) 
 	.collect(toList()); 

使用这种方式,你会得到一个List<CompletableFuture<String>>,列表中的每个CompletableFuture对象在计算完成后都包含商店的String类型的名称。但是,由于你用CompletableFutures实现的findPrices方法要求返回一个List<String>,你需要等待所有的future执行完毕,将其包含的值抽取出来,填充到列表中才能返回。

为了实现这个效果,你可以向最初的List<CompletableFuture<String>>施加第二个map操作,对List中的所有future对象执行join操作,一个接一个地等待它们运行结束。注意CompletableFuture类中的join方法和Future接口中的get有相同的含义,并且也声明在Future接口中,它们唯一的不同是join不会抛出任何检测到的异常。使用它你不再需要使用try/catch语句块让你传递给第二个map方法的Lambda表达式变得过于臃肿。所有这些整合在一起,你就可以重新实现findPrices了,具体代码如下:

public List<String> findPrices(String product) { 
 	List<CompletableFuture<String>> priceFutures = 
 		shops.stream() 
 		.map(shop -> CompletableFuture.supplyAsync( 
				 () -> shop.getName() + " price is " +
 					shop.getPrice(product))) 
		 .collect(Collectors.toList()); 
 	return priceFutures.stream() 
 		.map(CompletableFuture::join) 
 		.collect(toList()); 
} 

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

在这里插入图片描述

输出结果:
[BestPrice price is 123.26, LetsSaveBig price is 169.47, MyFavoriteShop price 
 is 214.13, BuyItAll price is 184.74] 
Done in 2005 msecs 

输出结果不尽人意。这种场景下使用CompletableFutures真的是浪费时间吗?

并行流的版本工作得非常好,那是因为它能并行地执行四个任务,所以它几乎能为每个商家分配一个线程。但是,如果你想要增加第五个商家到商店列表中,让你的“最佳价格查询”应用对其进行处理,处理结果会使得第三种方式更好。

关于CompletableFuture,还有更多的知识,以后有机会再做了解。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

探索Java8——CompletableFuture: 组合式异步编程 的相关文章

随机推荐

  • 用Python的Pandas和Matplotlib绘制股票KDJ指标线

    1 KDJ指标的计算过程 KDJ指标也叫随机指标 是由乔治 蓝恩博士 George Lane 最早提出的 该指标集中包含了强弱指标 动量概念和移动平均线的优点 可以用来衡量股价脱离正常价格范围的偏离程度 KDJ指标的计算过程是 首先获取指定
  • .有如下的4个/24地址块,试进行最大可能的聚合。212.56.132.0/24,212.56.133.0/24,212.56.134.0/24,212.56.135.0/24。

    有如下的4个 24地址块 试进行最大可能的聚合 212 56 132 0 24 212 56 133 0 24 212 56 134 0 24 212 56 135 0 24 由于四个地址块前两个字节都相同 只需将每个地址块的第三个字节转换
  • fedora系统更新时间

    先进行安装 yum install ntpdate 修改时区为上海 cp usr share zoneinfo Asia Shanghai etc localtime 之后运行两遍 ntpdate asia pool ntp org 使用c
  • 从Cortex-M33内核认识TrustZone

    欢迎大家关注STM32L5课程 本期我们会介绍STM32L5的内核 Cortex M33 它是ARM在MCU架构上增加了TrustZone这个安全扩展的一种内核实现 从这一期开始 我们进入技术部分的学习 L5快速入门 会由5期的介绍组成 会
  • git代码迁移后本地如何操作,如何变更为新的git仓库地址及重新配置用户名、密码

    git代码迁移后本地如何操作 如何变更为新的git仓库地址 答案是 直接切换git远程仓库地址即可 1 首先查看远程仓库的地址 git remote v 2 然后set url设置新的代码仓库地址 git remote set url or
  • .gitignore 文件和 .gitattributes 文件的使用

    每当想用 gitignore文件的时候 却发现已经push了不必要的文件 但如果你不慎在创建 gitignore文件之前就push了项目 那么即使你在 gitignore文件中写入新的过滤规则 这些规则也不会起作用 Git仍然会对所有文件进
  • cuda第一次计算耗时_FLUENT计算与GPU加速

    太长不看版本 结论如下 1 FLUENT中 GPU加速对于耦合求解器计算十分明显 3060ti能够提高计算效率约3倍 1080ti能够提高计算效率约2倍 2 FLUENT中 GPU加速对于分离式求解器效果不明显 这可能是由于网格数太少 GP
  • VirtualBox下Android-x86安装与基础配置

    虚拟机 Virtual Box 6 1 系统 android x86 64 8 1 r6 一 下载 Android x86 镜像 英文站 Android on x86 项目 中文站 安卓X86中文站 二 虚拟机配置 1 新建虚拟机 类型 L
  • 万能近似定理(universal approximation theorrm)

    神经网络的架构 architecture 指网络的整体结构 大多数神经网络被组织成称为层的单元组 然后将这些层布置成链式结构 其中每一层都是前一层的函数 在这种结构中 第一层由下式给出 第二层 第三层 以此类推 可以看出 每一层的主体都是线
  • Spring Framework与JDK版本对应关系

    最近在实践Spring项目时 发现无法通过注解的方式实现Bean容器管理 控制器报错信息为 Failed to read candidate component class 也就是注解扫描不了 在反复检查代码不存在问题后意识到可能是版本兼容
  • Java-主流框架—(10)Spring-微服务SpringBoot

    1 SpringBoot概述 SpringBoot提供了一种快速使用Spring的方式 基于约定优于配置的思想 可以让开发人员不必在配置与逻辑业务之间进行思维的切换 全身心的投入到逻辑业务的代码编写中 从而大大提高了开发的效率 Spring
  • 如何在mysql中创建学生信息表_数据库怎么创建学生信息表

    大家好 我是时间财富网智能客服时间君 上述问题将由我为大家进行解答 数据库创建学生信息表的方法是 1 新建表 单击数据库 studentDb 前图标 然后右键 表 文件包 单击 新建表 选项 进入 新建表 窗口 2 设定表标识字段id 填写
  • Vue项目安装core-js报错解决方案

    报错问题如下 出现这这种情况的多半是core js的版本不对 解决方案如下 亲测多次有效 1 安装cnpm npm install g cnpm registry https registry npm taobao org 2 查看cnpm
  • 浏览器页面后退,重新运行ajax

    问题描述 在浏览器页面后退时 也就是说你点击链接到一个页面 然后又点击后退按钮回到刚才的页面 结果发现jQuery的ajax GET请求不再执行了 解决方法 禁用ajax缓存 ajaxSetup cache false 吐槽 为了解决这个问
  • java中的String

    Java中的String类是一种复合数据类型 比较String类的是否相等也有2种办法 和equals 两种 String是一个系统定义的类 不是基本数据类型 有关字符串处理的方法非常多 有时候两个 一样 的字符串做相等的比较运算时会得到t
  • 华为OD机试真题-查找充电设备组合【2023Q1】【JAVA、Python、C++】

    题目描述 某个充电站 可提供n个充电设备 每个充电设备均有对应的输出功率 任意个充电设备组合的输出功率总和 均构成功率集合P的1个元素 功率集合P的最优元素 表示最接近充电站最大输出功率p max的元素 输入描述 输入为3行 第1行为充电设
  • 时序预测

    时序预测 MATLAB实现Bayes贝叶斯优化LSTM 长短期记忆神经网络 时间序列预测 预测效果一览
  • React - Websocket

    组件didMount调用 Store createWebSocket Math random Store url ws window backend server slice 7 apronMapWebsocket 这个要与后端提供的相同
  • C++函数重载、重写与重定义

    演示代码 include
  • 探索Java8——CompletableFuture: 组合式异步编程

    文章目录 Future接口 Future接口的局限性 使用 CompletableFuture 使用并行流对请求进行并行操作 使用 CompletableFuture 发起异步请求 如果你的意图是实现并发 而非并行 或者你的主要目标是在同一