如何编写优雅的异步代码 — CompletableFuture
前言
在我们的意识里,同步执行的程序都比较符合人们的思维方式,而异步的东西通常都不好处理。在异步计算的情况下,以回调表示的动作往往会分散在代码中,也可能相互嵌套在内部,如果需要处理其中一个步骤中可能发生的错误时,情况变得更加糟糕。Java 8 引入了很多的新特性,其中就包含了 CompletableFuture 类的引入,这让我们编写清晰可读的异步代码变得更加容易,该类功能非常强大,包含了超过 50 多个方法。。。
什么是 CompletableFuture
CompletableFuture
类的设计灵感来自于 Google Guava
的 ListenableFuture 类,它实现了 Future
和 CompletionStage
接口并且新增了许多方法,它支持 lambda,通过回调利用非阻塞方法,提升了异步编程模型。它允许我们通过在与主应用程序线程不同的线程上(也就是异步)运行任务,并向主线程通知任务的进度、完成或失败,来编写非阻塞代码。
为什么要引入 CompletableFuture
Java
的 1.5 版本引入了 Future
,你可以把它简单的理解为运算结果的占位符,它提供了两个方法来获取运算结果。
get()
:调用该方法线程将会无限期等待运算结果。get(long timeout, TimeUnit unit)
:调用该方法线程将仅在指定时间timeout
内等待结果,如果等待超时就会抛出TimeoutException
异常。
Future
可以使用 Runnable
或 Callable
实例来完成提交的任务,通过其源码可以看出,它存在如下几个问题:
- 阻塞 调用
get()
方法会一直阻塞,直到等待直到计算完成,它没有提供任何方法可以在完成时通知,同时也不具有附加回调函数的功能。 - 链式调用和结果聚合处理 在很多时候我们想链接多个
Future
来完成耗时较长的计算,此时需要合并结果并将结果发送到另一个任务中,该接口很难完成这种处理。 - 异常处理
Future
没有提供任何异常处理的方式。
以上这些问题在 CompletableFuture
中都已经解决了,接下来让我们看看如何去使用 CompletableFuture
。
如何创建 CompletableFuture
最简单的创建方式就是调用 CompletableFuture.completedFuture(U value)
方法来获取一个已经完成的 CompletableFuture
对象。
@Test
public void testSimpleCompletableFuture() {
CompletableFuture<String> completableFuture = CompletableFuture.completedFuture("Hello mghio");
assertTrue(completableFuture.isDone());
try {
assertEquals("Hello mghio", completableFuture.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
需要注意的是当我们对不完整的 CompleteableFuture
调用 get
方法的话,会由于 Future
未完成,因此 get
调用将永远阻塞,此时可以使用 CompletableFuture.complete
方法手动完成 Future
。
任务异步处理
当我们想让程序在后台异步执行任务而不关心任务的处理结果时,可以使用 runAsync
方法,该方法接收一个 Runnable
类型的参数返回 CompletableFuture<Void>
。
@Test
public void testCompletableFutureRunAsync() {
AtomicInteger variable = new AtomicInteger(0);
CompletableFuture<Void> runAsync = CompletableFuture.runAsync(() -> process(variable));
runAsync.join();
assertEquals(100, variable.get());
}
public void process(AtomicInteger variable) {
System.out.println(Thread.currentThread() + " Process...");
variable.set(100);
}
如果我们想让任务在后台异步执行而且需要获取任务的处理结果时,可以使用 supplyAsync
方法,该方法接收一个 Supplier<T>
类型的参数返回一个 CompletableFuture<T>
。
@Test
public void testCompletableFutureSupplyAsync() {
CompletableFuture<String> supplyAsync = CompletableFuture.supplyAsync(this::process);
try {
assertEquals("Hello mghio", supplyAsync.get()); // Blocking
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
}
public String process() {
return "Hello mghio";
}
看到这里你可能会有个问题,上面执行 runAsync
和 supplyAsync
任务的线程是从哪里来的、谁创建的呢?实际上它和 Java 8 中的 parallelStream
类似, CompletableFuture
也是从全局 ForkJoinPool.commonPool()
获得的线程中执行这些任务的。同时,上面的两个方法也提供了自定义线程池去执行任务,其实你如果去了解过 CompletableFuture
的源码的话,你会发现其 API
中的所有方法都有个重载的版本,有或没有自定义 Executor
执行器。
@Test
public void testCompletableFutureSupplyAsyncWithExecutor() {
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(2);
CompletableFuture<String> supplyAsync = CompletableFuture.supplyAsync(this::process, newFixedThreadPool);
try {
assertEquals("Hello mghio", supplyAsync.get()); // Blocking
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
}
public String process() {
return "Hello mghio";
}
链式调用和结果聚合处理
我们知道 CompletableFuture
的 get()
方法会一直阻塞
直到获取到结果,CompletableFuture
提供了 thenApply
、thenAccept
和 thenRun
等方法来避免这种情况,而且我们还可以添加任务完成后的回调通知。这几个方法的使用场景如下:
- thenApply 当我们如果要在从
Future
接收值后任务之前运行自定义的业务代码,然后要为此任务返回一些值时,则可以使用该方法 - thenAccept 如果我们希望在从
Future
接收到一些值后执行任务之前运行自定义的业务代码而不关心返回结果值时,则可以使用该方法 - thenRun 如果我们想在Future完成后运行自定义的业务代码,并且不想为此返回任何值时,则可以使用该方法
@Test
public void testCompletableFutureThenApply() {
Integer notificationId = CompletableFuture.supplyAsync(this::thenApplyProcess)
.thenApply(this::thenApplyNotify) // Non Blocking
.join();
assertEquals(new Integer(1), notificationId);
}
@Test
public void testCompletableFutureThenAccept() {
CompletableFuture.supplyAsync(this::processVariable)
.thenAccept(this::thenAcceptNotify) // Non Blocking
.join();
assertEquals(100, variable.get());
}
@Test
public void testCompletableFutureThenRun() {
CompletableFuture.supplyAsync(this::processVariable)
.thenRun(this::thenRunNotify)
.join();
assertEquals(100, variable.get());
}
private String processVariable() {
variable.set(100);
return "success";
}
private void thenRunNotify() {
System.out.println("thenRun completed notify ....");
}
private Integer thenApplyNotify(Integer integer) {
return integer;
}
private void thenAcceptNotify(String s) {
System.out.println(
String.format("Thread %s completed notify ....", Thread.currentThread().getName()));
}
public Integer thenApplyProcess() {
return 1;
}
如果有大量的异步计算,那么我们可以继续将值从一个回调传递到另一个回调中去,也就是使用链式调用方式,使用方式很简单。
@Test
public void testCompletableFutureThenApplyAccept() {
CompletableFuture.supplyAsync(this::findAccountNumber)
.thenApply(this::calculateBalance)
.thenApply(this::notifyBalance)
.thenAccept((i) -> notifyByEmail()).join();
}
private void notifyByEmail() {
// business code
System.out.println("send notify by email ...");
}
private Double notifyBalance(Double d) {
// business code
System.out.println(String.format("your balance is $%s", d));
return 9527D;
}
private Double calculateBalance(Object o) {
// business code
return 9527D;
}
private Double findAccountNumber() {
// business code
return 9527D;
}
比较细心的朋友可能注意到在所有前面的几个方法示例中,所有方法都是在同一线程上执行的。如果我们希望这些任务在单独的线程上运行时,那么我们可以使用这些方法对应的异步版本。
@Test
public void testCompletableFutureApplyAsync() {
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(2);
ScheduledExecutorService newSingleThreadScheduledExecutor = Executors
.newSingleThreadScheduledExecutor();
CompletableFuture<Double> completableFuture =
CompletableFuture
.supplyAsync(this::findAccountNumber,
newFixedThreadPool) // 从线程池 newFixedThreadPool 获取线程执行任务
.thenApplyAsync(this::calculateBalance,
newSingleThreadScheduledExecutor)
.thenApplyAsync(this::notifyBalance);
Double balance = completableFuture.join();
assertEquals(9527D, balance);
}
执行结果处理
thenCompose
方法适合有依赖性的任务处理,比如一个计算账户余额的业务:首先我们要先找到帐号,然后为该帐户计算余额,然后计算完成后再发送通知。所有这些任务都是依赖前一个任务的返回 CompletableFuture
结果,此时我们需要使用 thenCompose
方法,其实有点类似于 Java 8 流的 flatMap
操作。
@Test
public void testCompletableFutureThenCompose() {
Double balance = this.doFindAccountNumber()
.thenCompose(this::doCalculateBalance)
.thenCompose(this::doSendNotifyBalance).join();
assertEquals(9527D, balance);
}
private CompletableFuture<Double> doSendNotifyBalance(Double aDouble) {
sleepSeconds(2);
// business code
System.out.println(String.format("%s doSendNotifyBalance ....", Thread.currentThread().getName()));
return CompletableFuture.completedFuture(9527D);
}
private CompletableFuture<Double> doCalculateBalance(Double d) {
sleepSeconds(2);
// business code
System.out.println(String.format("%s doCalculateBalance ....", Thread.currentThread().getName()));
return CompletableFuture.completedFuture(9527D);
}
private CompletableFuture<Double> doFindAccountNumber() {
sleepSeconds(2);
// business code
System.out.println(String.format("%s doFindAccountNumber ....", Thread.currentThread().getName()));
return CompletableFuture.completedFuture(9527D);
}
private void sleepSeconds(int timeout) {
try {
TimeUnit.SECONDS.sleep(timeout);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
thenCombine
方法主要是用于合并多个独立任务的处理结果。假设我们需要查找一个人的姓名和住址,则可以使用不同的任务来分别获取,然后要获得这个人的完整信息(姓名 + 住址),则需要合并这两种方法的结果,那么我们可以使用 thenCombine
方法。
@Test
public void testCompletableFutureThenCombine() {
CompletableFuture<String> thenCombine = this.findName().thenCombine(this.findAddress(), (name, address) -> name + address);
String personInfo = thenCombine.join();
assertEquals("mghio Shanghai, China", personInfo);
}
private CompletableFuture<String> findAddress() {
return CompletableFuture.supplyAsync(() -> {
sleepSeconds(2);
// business code
return "Shanghai, China";
});
}
private CompletableFuture<String> findName() {
return CompletableFuture.supplyAsync(() -> {
sleepSeconds(2);
// business code
return "mghio ";
});
}
等待多个任务执行完成
在许多情况下,我们希望并行运行多个任务,并在所有任务完成后再进行一些处理。假设我们要查找 3 个不同用户的姓名并将结果合并。此时就可以使用 CompletableFuture
的静态方法 allOf
,该方法会等待所有任务完成,需要注意的是该方法它不会返回所有任务的合并结果,因此我们必须手动组合任务的执行结果。
@Test
public void testCompletableFutureAllof() {
List<CompletableFuture<String>> list = Lists.newArrayListWithCapacity(4);
IntStream.range(0, 3).forEach(num -> list.add(findName(num)));
CompletableFuture<Void> allFuture = CompletableFuture
.allOf(list.toArray(new CompletableFuture[0]));
CompletableFuture<List<String>> allFutureList = allFuture
.thenApply(val -> list.stream().map(CompletableFuture::join).collect(Collectors.toList()));
CompletableFuture<String> futureHavingAllValues = allFutureList
.thenApply(fn -> String.join("", fn));
String result = futureHavingAllValues.join();
assertEquals("mghio0mghio1mghio2", result);
}
private CompletableFuture<String> findName(int num) {
return CompletableFuture.supplyAsync(() -> {
sleepSeconds(2);
// business code
return "mghio" + num;
});
}
异常处理
在多线程中程序异常其实不太好处理,但是幸运的是在 CompletableFuture
中给我们提供了很方便的异常处理方式,在我们上面的例子代码中:
@Test
public void testCompletableFutureThenCompose() {
Double balance = this.doFindAccountNumber()
.thenCompose(this::doCalculateBalance)
.thenCompose(this::doSendNotifyBalance).join();
}
在上面的代码中,三个方法 doFindAccountNumber
、doCalculateBalance
和 doSendNotifyBalance
只要任意一个发生异常了,则之后调用的方法将不会运行。
CompletableFuture
提供了三种处理异常的方式,分别是 exceptionally
、handle
和 whenComplete
方法。第一种方式是使用 exceptionally
方法处理异常,如果前面的方法失败并发生异常,则会调用异常回调。
@Test
public void testCompletableFutureExceptionally() {
CompletableFuture<Double> thenApply = CompletableFuture.supplyAsync(this::findAccountNumber)
.thenApply(this::calculateBalance)
.thenApply(this::notifyBalance)
.exceptionally(ex -> {
System.out.println("Exception " + ex.getMessage());
return 0D;
});
Double join = thenApply.join();
assertEquals(9527D, join);
}
第二种方式是使用 handle
方法处理异常,使用该方式处理异常比上面的 exceptionally
方式更为灵活,我们可以同时获取到异常对象和当前的处理结果。
@Test
public void testCompletableFutureHandle() {
CompletableFuture.supplyAsync(this::findAccountNumber)
.thenApply(this::calculateBalance)
.thenApply(this::notifyBalance)
.handle((ok, ex) -> {
System.out.println("最终要运行的代码...");
if (ok != null) {
System.out.println("No Exception !!");
} else {
System.out.println("Exception " + ex.getMessage());
return -1D;
}
return ok;
});
}
第三种是使用 whenComplete
方法处理异常。
@Test
public void testCompletableFutureWhenComplete() {
CompletableFuture.supplyAsync(this::findAccountNumber)
.thenApply(this::calculateBalance)
.thenApply(this::notifyBalance)
.whenComplete((result, ex) -> {
System.out.println("result = " + result + ", ex = " + ex);
System.out.println("最终要运行的代码...");
});
}
总结
在本文中,介绍了 CompletableFuture
类的部分方法和使用方式,这个类的方法很多同时提供的功能也非常强大,在异步编程中使用的比较多,熟悉了基本的使用方法之后要深入了解还是要深入源码分析其实现原理。
如何编写优雅的异步代码 — CompletableFuture的更多相关文章
- 使用 Vert.X Future/Promise 编写异步代码
Future 和 Promise 是 Vert.X 4.0中的重要角色,贯穿了整个 Vert.X 框架.掌握 Future/Promise 的用法,是用好 Vert.X.编写高质量异步代码的基础.本文 ...
- 使用 Promises 编写更优雅的 JavaScript 代码
你可能已经无意中听说过 Promises,很多人都在讨论它,使用它,但你不知道为什么它们如此特别.难道你不能使用回调么?有什么了特别的?在本文中,我们一起来看看 Promises 是什么以及如何使用它 ...
- 无感知的用同步的代码编写方式达到异步IO的效果和性能,避免了传统异步回调所带来的离散的代码逻辑和陷入多层回调中导致代码无法维护
golang/goroutine 和 swoole/coroutine 协程性能测试对比 - Go语言中文网 - Golang中文社区 https://studygolang.com/articles ...
- 编写优雅代码,从挖掉恶心的if/else 开始
背景 长话短说, 作为开发人员经常需要根据条件灵活查询数据库,不管你是用rawsql 还是EFCore, 以下类似伪代码大家都可能遇到: /// <summary> /// 灵活查询 能耗 ...
- 终于可以愉快的撸Java异步代码了!
异步响应式编程可以极大的提高系统的并发呑吐量,但由于Java没有类似于其他语言的Async/Await机制,所以只能通过CompletableFuture.thenXXX()来串联各个异步任务,这 ...
- 🏆【Java技术专区】「并发编程专题」教你如何使用异步神器CompletableFuture
前提概要 在java8以前,我们使用java的多线程编程,一般是通过Runnable中的run方法来完成,这种方式,有个很明显的缺点,就是,没有返回值.这时候,大家可能会去尝试使用Callable中的 ...
- 一网打尽异步神器CompletableFuture
最近一直畅游在RocketMQ的源码中,发现在RocketMQ中很多地方都使用到了CompletableFuture,所以今天就跟大家来聊一聊JDK1.8提供的异步神器CompletableFutur ...
- co.js - 让异步代码同步化
近期在全力开发个人网站,并且又沉淀了一些前后端的技术.近期会频繁更新. 这篇文章首发于我的个人网站:听说 - https://tasaid.com/,建议在我的个人网站阅读,拥有更好的阅读体验. 这篇 ...
- node.js的作用、回调、同步异步代码、事件循环
http://www.nodeclass.com/articles/39274 一.node.js的作用 I/O的意义,(I/O是输入/输出的简写,如:键盘敲入文本,输入,屏幕上看到文本显示输出.鼠标 ...
随机推荐
- 提高 Web开发性能的 10 个方法
随着网络的高速发展,网络性能的持续提高成为能否在芸芸App中脱颖而出的关键.高度联结的世界意味着用户对网络体验提出了更严苛的要求.假如你的网站不能做到快速响应,又或你的App存在延迟,用户很快就会移情 ...
- JZOJ 3453.【NOIP2013中秋节模拟】连通块(connect)
3453.[NOIP2013中秋节模拟]连通块(connect) Time Limits: 1000 ms Memory Limits: 262144 KB (File IO): input:conn ...
- openwrt 为软件包添加服务
手动修改 rc.local 加入也可以实现自启动,缺点手动修改太麻烦,停止只能用 kill . 配置成服务最方便了,可以启用或禁用,启动,停止,重启非常方便. 在openwrt 中使用服务 servi ...
- 代号为 Kyria 的 Manjaro Linux 19.0 系统正式发布
Xfce版本仍然是主打,此版本Xfce更新到4.14,并且主要致力于在桌面和窗口管理器上完善用户体验. KDE版本提供了功能强大.成熟且丰富的Plasma 5.17桌面环境,此版本进行了完全重新设计. ...
- elementui 在原生方法参数里,添加参数
公司有个项目需求需要在一个列表中分别上传图片,饿了么的方法不支持传递index,可以这样传递: :on-change="(file,fileList)=>{return changeF ...
- Simulink仿真入门到精通(六) Simulink模型保存为图片
6.1 截图保存方式 Ctrl+Alt+A 6.2 拷贝试图方式 Edit→Copy Current View to Clipboard 6.3 saveas函数 用于保存figure或者simuli ...
- 单链表反转的原理和python代码实现
链表是一种基础的数据结构,也是算法学习的重中之重.其中单链表反转是一个经常会被考察到的知识点. 单链表反转是将一个给定顺序的单链表通过算法转为逆序排列,尽管听起来很简单,但要通过算法实现也并不是非常容 ...
- EPX-Studio脚本调用
procedure TF408017792.Button1Click(Sender: TObject); var NEPX: IExcelPanelXDisp; begin NEPX := this. ...
- HTTP请求中Get和Post请求的区别?
分类 Get的请求方式 1.直接在浏览器地址栏输入某个地址. 2.点击链接地址. 3.表单的默认提交方式或者设置为method="get". Post的请求方式 1.设置表单的me ...
- 【Weiss】【第03章】练习3.9:大整数运算包
[练习3.9] 编写任意精度的整数运算包,要求使用类似多项式运算的方法.计算24000内数字0到9的分布.