java8:使用CompletableFuture编写异步代码
原文地址:www.deadcoderising.com/java8-writi…
Java 8: Writing asynchronous code with CompletableFuture
java展现了许多非常cooooool的新特性,lambda表达式和流式操作非常引人注目。
CompletableFuture则可能被我们所忽略
你可能已经了解Futures
一个Future代表了未定的异步计算结果。它提供了一个方法——get()——当任务完成时它将返回计算的结果。
但是存在调用这个get方法会导致当前线程阻塞直到计算完成的问题,这个问题限制了异步运算而且将其变得毫无意义。
当然,你可以在所有场景下编写代码将任务交给线程池处理,但是,为什么你要担心非核心逻辑的事情呢
此时CompletableFuture站了出来
(原标题:This is where CompletableFuture saves the day)
它除了实现了 Future接口之外,还实现了 CompletionStage接口
CompletionStage是一个保证(promise),它保证运算最后必定能完成
CompletionStage有一个很棒的特性就是,它提供了丰富的方法可供选择,这些方法可以让你添加一些回调用于任务完成时触发
通过这个方式我们可以构建起非阻塞(non-blocking)的系统
好了,简单的介绍就到这里,我们开始写代码吧
一个最简单的异步计算
来让我们用一个很简单的基础语句开始——创建一个简单的异步计算
CompletableFuture.supplyAsync(this::sendMsg);
它很简单
supplyAsync传入一个包含我们想异步执行 的代码Supplier接口实例(在这个例子里面是一个sendMsg方法)
如果你曾经使用过一点Futures相关的功能,你或许想知道Executor在哪。如果你想,你仍可以在定义一个线程池当作第二个参数传入。但是如果你不去想这些,那么这些任务则会被提交到ForkJoinPool.commonPool()中执行
加入一个回调
我们的第一个异步任务已经完成了,让我们给他添加一个回调
回调的美妙之处在于,我们无需等待结果就能够说出当异步计算结束后应当发生什么
在第一个例子中,我们仅仅是简单的在它所在的线程通过执行 sendMsg方法异步地发送了一条消息
现在,我们添加一个回调函数告知信息是怎么发出的
CompletableFuture.supplyAsync(this::sendMsg)
.thenAccept(this::notify);
theAccept是添加一个回调的众多方法之一。它需要传入一个用于计算结束后处理先前运算结果的Consumer接口的实例(在这个例子中是notify)
添加多个链式回调
如果你想继续把值从一个回调传递到另一个中,thenAccept不会结束这个链,直到Consumer实例不再返回任何值为止。
为了传递值,你可以非常方便地使用thenApply替代它
thenApply需要传入一个用于接受值并且返回一个值的Function接口实例(译者:指将上一步结果映射为另一个值)
为了了解它是怎么工作的,让我们先找到一个接收者来扩展前面的示例
CompletableFuture.supplyAsync(this::findReceiver)
.thenApply(this::sendMsg)
.thenAccept(this::notify);
现在异步任务找到了一个接收者,然后在传递结果给最后一个回调告知我们之前,发送一条信息给这个接收者
构建异步系统
当我们构建一个更大的异步系统时,某些的工作方式会有些不同
你通常希望基于较小规模的代码组合新的代码。每一个部分将是非常典型的异步模式(在这个例子里面将返回多个CompetionStage)
直到现在,sendMsg还是一个普通的阻塞函数。现在假设有一个sendMsgAsync方法返回CompletionStage。
如果我们继续使用thenApply来编写上面的示例,我们将以嵌套的多个CompletionStage结束。
CompletableFuture.supplyAsync(this::findReceiver)
.thenApply(this::sendMsgAsync);
// Returns type CompletionStage<CompletionStage<String>>
//返回值类型为 CompletionStage<CompletionStage<String>>
这并不是我们期望的结果,所以我们可以使用thenCompose,这个可以允许我们提供一个Function接口的实例用于返回一个CompletionStage。它会有一个类似于 flatMap的“打平”效果
(译者:类似于将List<List<string>>转换为List)
CompletableFuture.supplyAsync(this::findReceiver)
.thenCompose(this::sendMsgAsync);
// Returns type CompletionStage<String>
//返回值类型为 CompletionStage<String>
这样我们就可以继续编写新的功能又不失去一个层次分明的CompletionStage
可以把回调作为单独任务的async后缀方法
直到目前为止我们所有的异步任务都在与前一个任务相同的的线程上执行的
只要你想,你可以将回调单独提交给ForkJoinPool.commonPool()而不是使用与前一个任务相同的线程。这是通过使用CompletionStage提供的方法的异步后缀版本来完成的。
假设我们想一次发送两条信息到同一个接收者
CompletableFuture<String> receiver
= CompletableFuture.supplyAsync(this::findReceiver);
receiver.thenApply(this::sendMsg);
receiver.thenApply(this::sendOtherMsg);
在上面这个例子里面,所有都是在相同线程中执行的。这将导致最后一条消息的发送需要等待第一条信息发送的结束
现在考虑这个代码
CompletableFuture<String> receiver
= CompletableFuture.supplyAsync(this::findReceiver);
receiver.thenApplyAsync(this::sendMsg);
receiver.thenApplyAsync(this::sendMsg);
通过使用带有async后缀的方法,每一个信息都被当做独立的任务提交给ForkJoinPool.commonPool()
这将意味着在完成前面的计算时同时执行sendMsg回调
这个的关键在于,当我们有多个回调基于相同的计算时,使用这个异步版本的方法非常方便
(译者:简单来说对于相同的CompletionStage 使用async后缀的多个任务是同步执行的,反之为顺序执行。请注意无论是使用thenApplyAsync还是thenApply返回实例的都与被调用的实例不是一个实例,也就是说CompletableFuture.supplyAsync().thenApplyAsync().thenApplyAsync()两个方法仍是顺序执行)
当它运行时异常怎么办
如你所知,坏的事情都可能发生。如果你曾经使用过Future,你就知道它有多糟糕。
幸运的是,CompletableFuture有个好的方式去捕获异常,那就是使用exceptionally
CompletableFuture.supplyAsync(this::failingMsg)
.exceptionally(ex -> new Result(Status.FAILED))
.thenAccept(this::notify);
exceptionally提供给我们一个机会通过使用一个替代函数来恢复,如果前面的运算失败并出现异常,这个替代函数就会执行
这样后续的回调函数就仍能够执行并把替代函数的结果当作输入
如果你需要更多的灵活性,看看whenComplete和handle是怎么用于处理异常的
(译者:
whenComplete获取上一步计算的计算结果和异常信息
handlehandle 方法和whenComplete方法类似,只不过接收的是一个 BiFunction<? super T,Throwable,? extends U> fn 类型的参数,因此有 whenComplete 方法和 转换的功能 (thenApply)
)
用可控的方式处理超时问题
超时是我们这些程序员经常需要考虑到的问题,有些时候我们不能等待计算完成
这也适用于CompletableFuture,我们需要一种方法告知我们的CompletableFuture我们能够等待多久和如果超时将应该做什么
在java9中通过使用两个可以让我们处理超时的新方法,这个问题便得到了解决,这两个方法就是orTimeout和completeOnTimeout
如果我们有一个被挂起的信息,并且我们不想一直等待它结束
So say we got a hanging message, and we don't want to wait forever for it to finish.
orTimeout所做的就是处理我们的CompletableFuture exceptionally,如果它没有在指定的时间内完成计算
CompletableFuture.supplyAsync(this::hangingMsg)
.orTimeout(1, TimeUnit.MINUTES);
好了,我们继续,如果被挂起的信息没有在一分钟内完成,将抛出一个 TimeoutEexception,这个异常可以被我们前面提到的exceptionally回调处理
另一个选择就是使用completeOnTimeout,它能够给我们提供其他替代值的能力
对我来说,这个方式比仅仅抛出异常要好,因为它允许我们用一个良好的受控方式修复异常
CompletableFuture.supplyAsync(this::hangingMsg)
.completeOnTimeout(new Result(Status.TIMED_OUT),1, TimeUnit.MINUTES);
现在,如果挂起的信息没有及时返回,我们就会返回一个拥有TIMED_OUT的Result
(译者:
var integer = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 1;
}).completeOnTimeout(2, 1, TimeUnit.SECONDS).get();
System.out.println(integer);
输出为2
)
基于多个运算结果的回调
有时候创建一个基于两个运算结果的回调是非常有帮助的。thenCombine可以将其变得十分遍历
thenCombine允许我们使用一个基于两个CompletionStage的 BiFunction实例的回调
为了理解它是怎么工作的,让我们除了寻找接收者以外,在发送信息前再去执行创建一些信件内容的繁重工作。
CompletableFuture<String> to =
CompletableFuture.supplyAsync(this::findReceiver);
CompletableFuture<String> text =
CompletableFuture.supplyAsync(this::createContent);
to.thenCombine(text, this::sendMsg);
首先,我们开启了两个异步任务——寻找接收者和创建信件内容,然后我们使用 thenCombine通过定义我们自己的BiFunction实例去表明我们想用两个计算的结果做什么。
值得一提的是,这里还存在另一种 thenCombine的变种——叫runAfterBoth。这个使用Runnable,而不关心先前的运算实际的值——只关心两个运行完实际做什么
取决于这个或另一个结果的回调
现在我们已经覆盖了你需要基于两个运算结果的情形,当我们仅仅只需要多个运算结果之一时,我们该怎么办
比如说,我们有两个获取一个接收者的方法。你可能会同时请求这两个,但是乐意见到第一个返回结果的。
CompletableFuture<String> firstSource =
CompletableFuture.supplyAsync(this::findByFirstSource);
CompletableFuture<String> secondSource =
CompletableFuture.supplyAsync(this::findBySecondSource);
firstSource.acceptEither(secondSource, this::sendMsg);
正如我们所看到的,通过 acceptEither使用两个需要等待的计算和一个使用第一个返回的结果的Comsumer接口实例,这个问题就能被轻易解决
(译者:
allOf(..) anyOf(..)
支持变长CompletableFuture参数
allOf:当所有的CompletableFuture都执行完后执行计算
anyOf:最快的那个CompletableFuture执行完之后执行计算
)
更多的信息
以上的已经基本覆盖了CompletableFuture提供的功能,但仍旧存在一些方法值得尝试,请查看文档获取更多的信息