作者:潘吉祥
CompletableFutur在jdk1.8被引入,目的是为了解决程序异步编排的复杂性。配合同为8版本引入的lambda表达式和streamAPI,可以编写足够优雅的异步程序。(有对jdk8的新特性不太了解的读者可以事先了解一下,本系列代码使用了大量1.8新特性,以便更好地理解示例程序,当然对于示例代码会有一些说明性文字)。
用CompletableFuture开发异步程序
为了避免文档API翻译式的学习,相对加深我们对CompletableFuture的了解,并快速把它应用到我们的实际开发中,笔者在这里创建一个“用户视频列表”的应用,它要根据用户展示对应的视频。希望在这整个示例演进改造过程中,读者能够学会以下的方法能力:
1 如何在你的项目中设计异步API。
2 如何把项目中现有的阻塞API变为异步,并合理地编排这些异步API。
3 如何以响应式的方式来处理异步操作完成之后的事件。
正文开始
1 首先我们需要为“用户视频列表”应用创建一个‘视频列表’方法,该方法返回平台所有的视频列表(假设目前只有1个视频)。
public class Video {
private String name;
public Video(String name) {
this.name = name;
}
// set get tostring方法略……
}
创建获取视频列表方法,并模拟查询时长
// 查询视频列表方法
public static List
由代码(示例代码使用了lambda和streamAPI)可知,获取一个视频需要耗时1秒,getVideos的调用者在调用此方法时会被阻塞。为了等待获取视频列表而阻塞1秒钟,这让我们觉得很不爽,因为要完成“用户视频列表”功能,我们还要调用用户信息、用户浏览记录和推荐计算API。
为了给用户更加流畅的体验,我们希望将getVideos变为异步。
同步变异步
因此,我们将getVideos重新改造为getVideosAsync,并返回一个Future包装:
// 异步查询视频列表方法
public static Future> getVideosAsync() {
// 创建CompletableFuture实例
CompletableFuture> futureVideos = new CompletableFuture<>();
// 开启新线程来执行查询
new Thread(() -> {
List videos = selectVideos();
// 查询完成后设置结果
futureVideos.complete(videos);
}).start();
// 非阻塞,直接返回包装结果
return futureVideos;
}
这里,我们首先创建了异步计算的CompletableFuture对象,然后创建了一个新的线程去执行视频列表查询selectVideos,在查询完毕后会设置值到CompletableFuture;我们直接返回CompletableFuture对象,返回动作是独立于查询线程,因此没有被阻塞。
异步方式测试
long start = System.nanoTime();
Future> videosAsync = Video.getVideosAsync();
long time = (System.nanoTime() - start) / 1_000_000;
System.out.println("视频列表查询异步返回耗时 " + time + "毫秒");
// 比如查询用户信息,查询用户浏览记录等
doSomethingOther();
try {
// 阻塞方法
List videos = videosAsync.get();
System.out.println(videos);
} catch (Exception e) {
e.printStackTrace();
}
long resultTime = (System.nanoTime() - start) / 1_000_000;
System.out.println("视频列表查询在" + resultTime + " 毫秒后返回结果");
返回结果如下
视频列表查询异步返回耗时 46毫秒
[Video{夏洛特}]
视频列表查询在1053 毫秒后返回结果
我们可以发现,Video.getVideosAsync()在很短时间内就返回了,并没有阻塞当前方法的doSomethingOther。细心的读者可能发现,我们调用了videosAsync.get()方法,此方法意在获取异步包装对象中的值,属于阻塞方法,假如说我们的查询视频列表方法迟迟没有返回,它会永久阻塞在这里。因此我们开发中建议使用他的重载方法(V get(long timeout, TimeUnit unit)),传入超时时长,避免无限阻塞。
除此之外,另一个可能的情况是,查询列表方法报错了,我们就面临另一个问题:如何管理我们异步任务可能出现的错误,这在开发中是不容忽视的一个问题。
异步任务的错误处理
如果我们的异步视频列表查询方法报错,这个错误会被限制在试图查询的异步线程中,并终结该线程。这依然会导致调用get方法的消费端被永久阻塞(假如没有使用超时参数)。
但即使客户端使用了超时参数,也只会收到一个Timeout-Exception,仅此而已,至于查询视频列表的异步方法中到底发生的什么错误,我们还是一无所知。为此,我们需要使用CompletableFuture的completeExceptionally方法,此方法可以将异步任务中的错误抛出。改造如下:
抛出异步任务中的错误
// 异步查询视频列表方法
public static Future> getVideosAsync() {
// 创建CompletableFuture实例
CompletableFuture> futureVideos = new CompletableFuture<>();
// 开启新线程来执行查询
new Thread(() -> {
try {
List videos = selectVideos();
// 查询完成后设置结果
futureVideos.complete(videos);
}catch (Exception e) {
// 抛出错误
futureVideos.completeExceptionally(e);
}
}).start();
// 非阻塞,直接返回包装结果
return futureVideos;
}
现在同样的get调用,如果异步任务报错,get调用方回收到一个ExecutionException(执行异常)异常,该异常包装了异步线程中真正发生的异常。
假如发生了一个"videos not available"异常,那个调用方(即我们的测试线程)会打印这样的信息:
java.util.concurrent.ExecutionException: java.lang.RuntimeException: videos not available
at java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:357)
at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1895)
Caused by: java.lang.RuntimeException: videos not available
at java8.Video.lambda$getVideosAsync$0(Video.java:42)
at java.lang.Thread.run(Thread.java:748)
到现在为止,我们已经学会使用CompletableFuture进行异步方法调用,看起来也很简单。但事实上CompletableFuture本身为开发者提供了大量方便优雅的工厂方法,直接使用这些方法,配合1.8的新特性,能够构建更加优雅的异步应用。
点个关注,关于CompletableFuture更好实践,参见异步编程实践第三话。
【推荐阅读】
字节跳动CEO张一鸣卧底公司群2天,怒斥员工摸鱼~
为什么MySQL不推荐使用uuid或者雪花id作为主键?
List 去除重复数据的 5 种正确姿势!
基于 Token 的多平台身份认证架构设计
超美观的 Vue+Element 开源后台管理 UI
图解 Docker 架构
面试官问:BitMap了解么?在什么场景下用过?
Docker 图形化工具:Portainer
我终于决定要放弃okhttp、httpClient,选择了这个牛逼的神仙工具!贼爽