详解 java CompletableFuture
背景知识
要理解 CompletableFuture,首先要弄懂什么是 Future。因为后者是前者的扩展。本文并不打算详细的介绍 Future,毕竟不是本文的重点。
Future是java1.5增加的一个接口,提供了一种异步并行计算的能力。比如说主线程需要执行一个复杂耗时的计算任务,我们可以通过future把这个任务放在独立的线程(池)中执行,然后主线程继续处理其他任务,处理完成后再通过Future获取计算结果。
这里通过一个简单的示例带你理解下 Future。
我们有两个服务,一个是用户服务可以获取用户信息,一个是地址服务,可以通过用户id获取地址信息。如下,
@AllArgsConstructor
@Data
public class PersonInfo {
private long id;
private String name;
private int age;
}
public class PersonService {
public PersonInfo getPersonInfo(Long personId) throws InterruptedException {
Thread.sleep(500);//模拟调用耗时
//真实项目中,这里大部分时候是通过dao从数据库获取
return new PersonInfo(personId, "zhangsan", 30); //返回一条
}
}
@AllArgsConstructor
public class AddressInfo {
private String addressLine;
private String city;
private String province;
private String country;
}
public class AddressService {
public AddressInfo getAddress(long personId) throws InterruptedException {
Thread.sleep(600); //模拟调用耗时
System.out.println("id:" + personId);
//真实项目中,这里大部分时候是通过dao从数据库获取
return new AddressInfo("胜利大街143号", "北京市", "北京", "中国");
}
}
然后我们演示下如何在主线程使用 Future 来进行异步调用。
public class FutureTest {
public static void main(String[] args) throws InterruptedException, ExecutionException {
long startTime = System.currentTimeMillis();
PersonService personService = new PersonService();
AddressService addressService = new AddressService();
long personId = 100L;
//调用用户服务获取用户基本信息
FutureTask<PersonInfo> personInfoFutureTask = new FutureTask<>(new Callable<PersonInfo>() {
@Override
public PersonInfo call() throws Exception {
return personService.getPersonInfo(personId);
}
});
new Thread(personInfoFutureTask).start();
Thread.sleep(300); //模拟主线程其它操作耗时
FutureTask<AddressInfo> addressInfoFutureTask = new FutureTask<>(new Callable<AddressInfo>() {
@Override
public AddressInfo call() throws Exception {
return addressService.getAddress(personId);
}
});
new Thread(addressInfoFutureTask).start();
PersonInfo personInfo = personInfoFutureTask.get();//获取个人信息结果
AddressInfo addressInfo = addressInfoFutureTask.get();//获取地址信息结果
System.out.println("总共用时" + (System.currentTimeMillis() - startTime) + "ms");
}
}
输出:
总共用时909ms
很明显,如果我们不使用 Future,而是在主线程串行的调用,耗时会是 500 + 300 + 600 = 1400
毫秒。通过 Future提供的异步计算功能,我们可以多个任务并行的执行,从而提高执行效率。
我希望你能仔细的看上面的这个示例,因为后面讲到 CompletableFuture 我会使用同一个示例。
基本介绍
通过上面的例子来看,似乎 Future 本身已经很强大了。那么 CompletableFuture 又是做啥的呢?
虽然Future以及相关使用方法提供了异步执行任务的能力,但是对于结果的获取却是很不方便,只能通过阻塞或者轮询的方式得到任务的结果。在上面的示例中,personInfoFutureTask.get()
就是阻塞调用,在线程获取结果之前get方法会一直阻塞。
轮询的方式在上面的示例中没有,其实也很简单。是Future提供了一个isDone方法,我们可以在程序中不断的轮询这个方法查询执行结果。
但是,无论是阻塞方式还是轮询方式,都不够好。
- 阻塞的方式和异步编程的初衷相违背
- 轮询的方式会耗费无谓的CPU资源
正是在这样的背景下,CompletableFuture在java8横空出世。CompletableFuture提供了一种机制可以让任务执行完成后通知监听的一方,类似设计模式中的观察者模式。
使用示例
首先我们来看看,上面Future那个示例,如果是用 CompletableFuture 该怎么做?
我先给出代码,
public class CompetableFutureTest {
public static void main(String[] args) throws InterruptedException, ExecutionException {
long startTime = System.currentTimeMillis();
PersonService personService = new PersonService();
AddressService addressService = new AddressService();
long personId = 100L;
CompletableFuture<PersonInfo> personInfoCompletableFuture = CompletableFuture.supplyAsync(() -> personService.getPersonInfo(personId));
CompletableFuture<AddressInfo> addressInfoCompletableFuture = CompletableFuture.supplyAsync(() -> addressService.getAddress(personId));
Thread.sleep(300); //模拟主线程其它操作耗时
PersonInfo personInfo = personInfoCompletableFuture.get();//获取个人信息结果
AddressInfo addressInfo = addressInfoCompletableFuture.get();//获取地址信息结果
System.out.println("总共用时" + (System.currentTimeMillis() - startTime) + "ms");
}
}
首先我们实现同样的功能代码简洁很多。supplyAsync 支持异步地执行我们指定的方法,这个例子中的异步执行方法是调用service。我们也可以使用 Executor 执行异步程序,默认是 ForkJoinPool.commonPool()。
另外通过这个示例,可以发现我们完全可以使用 CompletableFuture 代替 Future。
当然 CompletableFuture 的功能远不止与此,不然它的存在就没有意义了。CompletableFuture 提供了几十种方法辅助我们操作异步任务,用好了这些方法可以写出更加简洁,高效的代码。比如下面这个例子:
CompletableFuture<PersonInfo> personInfoCompletableFuture = CompletableFuture.supplyAsync(() -> personService.getPersonInfo(personId));
CompletableFuture<AddressInfo> addressInfoCompletableFuture = CompletableFuture.supplyAsync(() -> addressService.getAddress(personId));
final CompletableFuture<Void> completableFutureAllOf =
CompletableFuture.allOf(personInfoCompletableFuture, addressInfoCompletableFuture);
completableFutureAllOf.get(); //执行时间以最长那个任务为准
PersonInfo personInfo = personInfoCompletableFuture.get();//马上返回
AddressInfo addressInfo = addressInfoCompletableFuture.get();//马上返回
这个示例中,allOf可以让我们把多个异步任务结果的获取整合起来,这样操作更简单,代码更简洁。
前面提到了它可以解决的痛点,就是提供了一种类似观察者模式的机制,当异步的计算结果完成后可以通知监听者。下面来看个示例,
CompletableFuture<PersonInfo> personInfoCompletableFuture = CompletableFuture.supplyAsync(() -> personService.getPersonInfo(personId));
CompletableFuture<AddressInfo> addressInfoCompletableFuture = CompletableFuture.supplyAsync(() -> addressService.getAddress(personId));
final CompletableFuture<Void> completableFutureAllOf =
CompletableFuture.allOf(personInfoCompletableFuture, addressInfoCompletableFuture);
//监听执行结果,整合两个任务的结果进一步处理
final CompletableFuture<PersonAndAddress> personAndAddressCompletableFuture = completableFutureAllOf.thenApply((voidInput) ->
new PersonAndAddress(personInfoCompletableFuture.join(), addressInfoCompletableFuture.join()));
personAndAddressCompletableFuture.join();//以时间长的任务为准
在上面这个示例中,当两个异步任务执行完毕后,我们可以通过thenApply监听到结果并进行处理。
CompletableFuture 还有很多好玩有用的功能,如果感兴趣可以自行研究下。
总结
通过前面的讲解,你应该对 Future 以及它的扩展接口 CompletableFuture 都有了比较深入的认识。
我个人的建议是如果你的项目是基于java8,大部分情况你应该用后者而不是前者。如果你的项目是java8之前的版本,也建议你使用第三方的工具比如 Guava 等框架提供的Future工具类。
参考:
- https://www.ibm.com/developerworks/cn/java/j-cf-of-jdk8/index.html
- 接口测试 | 21 基于flask弄个restful API服务出来
- 数论部分第二节:埃拉托斯特尼筛法 埃拉托斯特尼筛法
- [接口测试 -基础篇] 20 用flask写一个简单server用于接口测试
- 接口测试 | urllib篇 19 urllib基本示例
- 接口测试 | urllib篇 18 urllib介绍
- 【专知-Deeplearning4j深度学习教程01】分布式Java开源深度学习框架DL4j安装使用: 图文+代码
- .Net Core Runtime安装说明
- 【专知-Deeplearning4j深度学习教程02】用ND4J自己动手实现RBM: 图文+代码
- 【专知-Deeplearning4j深度学习教程03】使用多层神经网络分类MNIST数据集:图文+代码
- TypeScript 1.6发布:完全支持React/JSX
- 【专知-Java Deeplearning4j深度学习教程04】使用CNN进行文本分类:图文+代码
- sql server之数据库语句优化
- 【专知-Java Deeplearning4j深度学习教程05】无监督特征提取神器—AutoEncoder:图文+代码
- 平衡树初阶——AVL平衡二叉查找树+三大平衡树(Treap + Splay + SBT)模板【超详解】
- java教程
- Java快速入门
- Java 开发环境配置
- Java基本语法
- Java 对象和类
- Java 基本数据类型
- Java 变量类型
- Java 修饰符
- Java 运算符
- Java 循环结构
- Java 分支结构
- Java Number类
- Java Character类
- Java String类
- Java StringBuffer和StringBuilder类
- Java 数组
- Java 日期时间
- Java 正则表达式
- Java 方法
- Java 流(Stream)、文件(File)和IO
- Java 异常处理
- Java 继承
- Java 重写(Override)与重载(Overload)
- Java 多态
- Java 抽象类
- Java 封装
- Java 接口
- Java 包(package)
- Java 数据结构
- Java 集合框架
- Java 泛型
- Java 序列化
- Java 网络编程
- Java 发送邮件
- Java 多线程编程
- Java Applet基础
- Java 文档注释