CompletableFuture原理及应用场景详解

1.应用场景
现在我们打开各个APP上的一个页面,可能就需要涉及后端几十个服务的API调用,比如某宝、某个外卖APP上,下面是某个外卖APP的首页。首页上的页面展示会关联很多服务的API调用,如果使用同步调用的方式,接口耗时完全不能满足需求,因此,需要用到异步调用的方式。

2.使用线程池的弊端
说起异步调用,我们通常是创建一个线程池来实现多个请求的并行调用,这样接口的整体耗时由执行时间最长的线程决定。

但是线程池存在的问题是资源利用率较低:
- CPU资源大量浪费在阻塞等待上
- CPU调度的线程数增加了,在上下文切换上的资源消耗更大了。而且线程本身也占用系统资源
3.CompletableFuture的特性
我们引入CompletableFuture对业务流程进行编排,降低依赖之间的阻塞。本文主要讲述CompletableFuture的使用和原理。并对比Future、CompletableFuture、RxJava、Reactor的特性
| Future | CompletableFuture | RxJava | Reactor | |
|---|---|---|---|---|
| Composable(可组合) | ️ | ️ | ️ | |
| Asynchronous(异步) | ️ | ️ | ️ | ️ |
| Operator fusion(操作融合) | ️ | ️ | ||
| Lazy(延迟执行) | ️ | ️ | ||
| Backpressure(回压) | ️ | ️ |
- 可组合:将多个依赖操作通过不同方式进行编排,例如CompletableFuture提供thenCompose、thenCombine等方法,这些方法支持了可组合的特性
- 操作融合:将数据流中的多个操作符以某种方式结合起来,进而降低开销
- 延迟执行:操作不会立即执行,当收到明确指示时才会触发操作
- 回压:异步阶段的处理速度跟不上,直接失败会导致大量数据丢失,这是需要反馈上游生产者降低调用量
RxJava和Reactor虽然功能更强大,但是学习成本也更高,我们选择学习成本较低的CompletableFuture
4 一个例子回顾Future
CompletableFuture是由Java 8引入的,在Java8之前我们一般通过Future实现异步,而Future是Java5新加的接口,提供异步并行计算的功能
- Future只能通过阻塞或者轮询的方式获取结果,且不支持设置回调方法
- Future.get()方法是阻塞调用获取结果,还提供了isDone方法,在程序中轮询这个方法可查询执行结果
创建任务方法类
public class UserService {
public String getUserInfo() throws InterruptedException {
Thread.sleep(300L);
return "getUserInfo() 返回结果";
}
public String getUserAddress() throws InterruptedException {
Thread.sleep(500L);
return "getUserAddress() 返回结果";
}
}
创建Future测试
public class FutureTest {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
UserService userService = new UserService();
try {
Long start = System.currentTimeMillis();
Future<String> future1 = executor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return userService.getUserInfo();
}
});
Future<String> future2 = executor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return userService.getUserAddress();
}
});
String result1 = future1.get();
System.out.println(result1);
String result2 = future2.get();
System.out.println(result2);
System.out.println("两个任务执行耗时:" + (System.currentTimeMillis() - start) + " ms");
} catch (Exception e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
}
}
最后执行结果为:
getUserInfo() 返回结果
getUserAddress() 返回结果
两个任务执行耗时:505 ms
使用Future后任务的整体耗时,由最长的任务耗时决定
前面也说过,Future对结果的获取不友好,没有提供回调方法,只能阻塞或者轮询的方式。
Java8之前也可以用guava的ListenableFuture,来设置回调,但是这样又会导致臭名昭著的回调地狱(异步编程中因多层嵌套回调函数导致的代码可读性、可维护性急剧下降的现象),这里不展开了
5.CompletableFuture的使用
CompletableFuture实现了两个接口:Future和CompletionStage,Future用于异步计算,CompletionStage用于表示异步执行过程汇总的一个步骤Stage

5.1一个例子入门CompletableFuture
这里创建一个流程,多个任务之间存在依赖关系
根据依赖数量,可以分为:零依赖、一元依赖、二元依赖、多元依赖

5.1.1零依赖:创建异步任务

上面两个任务CF1、CF2就是零依赖,可以直接创建,主要有三种创建方式:
ExecutorService executor = Executors.newFixedThreadPool(5);
UserService userService = new UserService();
//1、使用runAsync或supplyAsync发起异步调用
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() ->
userService.getUserInfo(), executor);
//2、CompletableFuture.completedFuture()直接创建一个已完成状态的CompletableFuture
CompletableFuture<String> cf2 = CompletableFuture.completedFuture("result2");
//3、先初始化一个未完成的CompletableFuture,然后通过complete() completeExceptionally(),完成该CompletableFuture
CompletableFuture<String> cf = new CompletableFuture<>();
cf.complete("success");
5.1.2 一元依赖:依赖一个CF
任务的执行存在一个上游依赖,可以通过thenApply、thenAccept、thenCompose方法来实现

CompletableFuture<String> cf3 = cf1.thenApply(result1 -> {
//result1为CF1的结果
//......
return "result3";
});
CompletableFuture<String> cf5 = cf2.thenApply(result2 -> {
//result2为CF2的结果
//......
return "result5";
});
5.1.3 二元依赖:依赖两个CF
上图中的CF4就是个二元依赖,它依赖CF1和CF2,我们通过thenCombine等回调来实现。代码如下:
CompletableFuture<String> cf4 = cf1.thenCombine(cf2, (result1, result2) -> {
//result1和result2分别为cf1和cf2的结果
return "result4";
});
5.1.4 多元依赖:依赖多个CF

CF6是多元依赖,这种关系可以通过allOf、anyOf方法来实现:
allOf方法:多个依赖需全部完成anyOf方法:任意一个依赖完成即可
//多元依赖
CompletableFuture<Void> cf6 = CompletableFuture.allOf(cf3, cf4, cf5);
CompletableFuture<String> result = cf6.thenApply(v -> {
//这里的join并不会阻塞,因为传给thenApply的函数是在CF3、CF4、CF5全部完成时,才会执行 。
String result3 = cf3.join();
String result4 = cf4.join();
String result5 = cf5.join();
//根据result3、result4、result5组装最终result;
return result3 + result4 + result5;
});
6.CompletableFuture原理
CompletableFuture包含了两个volatile修饰的变量:result和stack
- result存储当前CF的结果
- stack表示当前CF完成后需要触发的依赖动作,依赖动作可以有多个,以栈形成存储,stack表示栈顶元素
volatile Object result; // Either the result or boxed AltResult
volatile Completion stack; // Top of Treiber stack of dependent actions
Completion类本身是观察者的基类

被观察者:每个CF都是一个被观察者,stack中存储的是注册的所有观察者,当CF执行完成后,会弹栈stack,依次通知观察者。result用于存储CF执行的结果数据
观察者:回调方法如thenApply、thenAccept会生成一个Completion类型的对象,就是观察者。检查当前CF是否已完成,如果已完成则执行Completion,否则加入观察者链stack中
7.使用问题
7.1代码执行在哪个线程上?
CompletableFuture的组合操作都有同步和异步两种方法:
同步方法(即不带Async后缀的):
- 如果注册时被依赖的操作已经执行完成,则直接由当前线程执行
- 如果注册时被依赖操作未执行完,则由回调线程执行
异步方法(带Async后缀的):
- 不传递线程池参数Executor时,由公共线程池CommonPool(CPU核数-1)执行
- 传递时用的传入的指定线程池
7.2异步回调要传线程池
异步回调时强制传入线程池,并根据实际情况做线程池隔离
不传递时,使用的都是公共线程池CommonPool,容易形成性能瓶颈。手动传递线程池参数可以更方便调节参数,并给不同业务分配不同线程池,做到资源隔离
7.3 Future需要获取返回值,才能获取异常信息
CompletableFuture<Void> future = CompletableFuture.supplyAsync(
......
)
//如果不加get()方法这一行,看不到异常信息
future.get();
Future需要获取返回值时,才能获取到异常信息,不加get()方法是看不到的。
CompletableFuture还提供了异常捕获回调exceptionally方法,相当于同步调用中的try/catch方法可获取异常
public CompletableFuture<Integer> getCancelTypeAsync(long orderId) {
CompletableFuture<WmOrderOpRemarkResult> remarkResultFuture = wmOrderAdditionInfoThriftService.findOrderCancelledRemarkByOrderIdAsync(orderId);//业务方法,内部会发起异步rpc调用
return remarkResultFuture
.exceptionally(err -> {//通过exceptionally 捕获异常,打印日志并返回默认值
log.error("WmOrderRemarkService.getCancelTypeAsync Exception orderId={}", orderId, err);
return 0;
});
}
CompletableFuture原理及应用场景详解的更多相关文章
- Java 读写锁 ReadWriteLock 原理与应用场景详解
Java并发编程提供了读写锁,主要用于读多写少的场景,今天我就重点来讲解读写锁的底层实现原理@mikechen 什么是读写锁? 读写锁并不是JAVA所特有的读写锁(Readers-Writer Loc ...
- Android辅助功能原理与基本使用详解-AccessibilityService
辅助功能原理与基本使用详解 本文主要介绍辅助功能的使用 辅助功能基本原理 辅助功能基本配置和框架搭建 辅助功能实战解析 辅助功能基本原理 辅助功能(AccessibilityService)其实是 ...
- [Spark内核] 第40课:CacheManager彻底解密:CacheManager运行原理流程图和源码详解
本课主题 CacheManager 运行原理图 CacheManager 源码解析 CacheManager 运行原理图 [下图是CacheManager的运行原理图] 首先 RDD 是通过 iter ...
- Nginx 反向代理工作原理简介与配置详解
Nginx反向代理工作原理简介与配置详解 by:授客 QQ:1033553122 测试环境 CentOS 6.5-x86_64 nginx-1.10.0 下载地址:http://nginx. ...
- 转:修改ETM,用Ogre实现《天龙八部》地形与部分场景详解
本文主要讲的是<天龙八部>游戏的地形和一部分场景的具体实现,使用C++, Ogre1.6,我摸索了段时间,可能方法用的并不是最好的,但好歹实现了.文章可能讲得有点罗嗦,很多简单的东西都讲了 ...
- “全栈2019”Java第一百一十三章:什么是回调?回调应用场景详解
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...
- (转)使用LVS实现负载均衡原理及安装配置详解
使用LVS实现负载均衡原理及安装配置详解 原文:https://www.cnblogs.com/liwei0526vip/p/6370103.html
- 使用LVS实现负载均衡原理及安装配置详解
负载均衡集群是 load balance 集群的简写,翻译成中文就是负载均衡集群.常用的负载均衡开源软件有nginx.lvs.haproxy,商业的硬件负载均衡设备F5.Netscale.这里主要是学 ...
- B-index、bitmap-index、text-index使用场景详解
索引的种类:B-tree索引.Bitmap索引.TEXT index1. B-tree索引介绍: B-tree 是一种常见的数据结构,也称多路搜索树,并不是二叉树.B-tree 结构可以显著减少定位 ...
- 深入了解View实现原理以及自定义View详解
下面几篇文章对View的原理讲的非常详细. Android LayoutInflater原理分析,带你一步步深入了解View(一) Android视图绘制流程完全解析,带你一步步深入了解View(二) ...
随机推荐
- 2024新版本如何配置CLion与cubeMX开发STM32
2024新版本如何配置CLion与cubeMX开发STM32 1. 为什么我要在网上有很多教程的情况下再做一个新版 各种大佬们给出的配置教程原本很详细,但是在时间过了这么久之后已经不完全符合现在的环境 ...
- Spring-RetryTemplate-RestTemplate的使用
------------------------------------------------------------------------------------ 1.基本概念 1.1应用场景 ...
- java代码之美(1)
Lambda 一.概述 1.什么是Lambda表达式 Lambda 表达式是一种匿名函数,简单地说,它是没有声明的方法,也即没有访问修饰符.返回值声明和名字. 它可以写出更简洁.更灵活的代码.作为一种 ...
- 使用pict生成正交表
使用pict可以自动生成正交表 步骤: 1.安装 2.新建TXT文件,格式为: 因子1:水平项1,水平项2 因子2:水平项1,水平项2 多个水平项用英文逗号分割 3.生成,打开cmd,切换到要 ...
- AVX512
最近接触到SIMD编码,就不可避免的查到了AVX指令集,两者有什么关系呢,了解一下? 问:AVX是什么? 答:是一套指令集 下面具体看: AVX 以下内容主要转载自:AVX指令集是什么?它的应用又有哪 ...
- 一文读懂ROS开发,解锁RK3562J + Ubuntu工业平台应用
在工业智能化浪潮中,智能机器人设备是成为工业自动化体系的璀璨之星,而其核心 --ROS系统,更是机器人领域的集大成者.今天,和大家分享一个ROS开发案例,基于RK3562J + Ubuntu工业平台. ...
- DBeaver出现“Public Key Retrieval is not allowed”错误的解决办法
1.问题描述 我们在使用DBeaver连接MySql的时候,可能会出现"Public Key Retrieval is not allowed"的错误提示,如下图所示: 2.解决办 ...
- 1分钟学会DeepSeek本地部署,小白也能搞定!
DeepSeek 是国内顶尖 AI 团队「深度求索」开发的多模态大模型,具备数学推理.代码生成等深度能力,堪称"AI界的六边形战士". DeepSeek 身上的标签有很多,其中最具 ...
- Atcoder ABC383E Sum of Max Matching 题解 [ 绿 ] [ 最小瓶颈路 ] [ 并查集 ] [ Kruskal 重构树 ]
Sum of Max Matching:简单贪心,但我场上没切,唐完了. 思路 显然,对于最大边权最小问题,首先想到最小瓶颈路的 trick:按边的大小排序,对原图进行加边. 同时可以发现,这个匹配有 ...
- C#进行word模板占位符替换的几种工具
word模板中,包含一些需要替换的项,比如{{姓名}} {{年龄}}或者$姓名$ $年龄$,从数据库获取信息后,对模板进行替换操作生成新的word文档. 简单对以下四种工具做了一下测试: 1.NPOI ...