面试官:五年开发连异步编程CompletableFuture都不会
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<Video> getVideos() {
return selectVideos();
}
final static List<String> videos = Arrays.asList("夏洛特");
public static List<Video> selectVideos() {
// 由数据源videos获取一个stream流,相当于内部循环
List<Video> videoList = videos.stream()
// 将遍历的视频名字映射为Video对象
.map(name -> {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return new Video(name);
// 最后将映射的Video对象收集为一个列表
}).collect(Collectors.toList());
return videoList;
}
同步变异步
// 异步查询视频列表方法
public static Future<List<Video>> getVideosAsync() {
// 创建CompletableFuture实例
CompletableFuture<List<Video>> futureVideos = new CompletableFuture<>();
// 开启新线程来执行查询
new Thread(() -> {
List<Video> videos = selectVideos();
// 查询完成后设置结果
futureVideos.complete(videos);
}).start();
// 非阻塞,直接返回包装结果
return futureVideos;
}
异步方式测试
long start = System.nanoTime();
Future<List<Video>> videosAsync = Video.getVideosAsync();
long time = (System.nanoTime() - start) / 1_000_000;
System.out.println("视频列表查询异步返回耗时 " + time + "毫秒");
// 比如查询用户信息,查询用户浏览记录等
doSomethingOther();
try {
// 阻塞方法
List<Video> 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 毫秒后返回结果
异步任务的错误处理
如果我们的异步视频列表查询方法报错,这个错误会被限制在试图查询的异步线程中,并终结该线程。这依然会导致调用get方法的消费端被永久阻塞(假如没有使用超时参数)。
但即使客户端使用了超时参数,也只会收到一个Timeout-Exception,仅此而已,至于查询视频列表的异步方法中到底发生的什么错误,我们还是一无所知。为此,我们需要使用CompletableFuture的completeExceptionally方法,此方法可以将异步任务中的错误抛出。改造如下:
抛出异步任务中的错误
// 异步查询视频列表方法
public static Future<List<Video>> getVideosAsync() {
// 创建CompletableFuture实例
CompletableFuture<List<Video>> futureVideos = new CompletableFuture<>();
// 开启新线程来执行查询
new Thread(() -> {
try {
List<Video> 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)
【推荐阅读】