vlambda博客
学习文章列表

面试官:五年开发连异步编程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;
}


由代码(示例代码使用了lambda和streamAPI)可知,获取一个视频需要耗时1秒,getVideos的调用者在调用此方法时会被阻塞。为了等待获取视频列表而阻塞1秒钟,这让我们觉得很不爽,因为要完成“用户视频列表”功能,我们还要调用用户信息、用户浏览记录和推荐计算API。
为了给用户更加流畅的体验,我们希望将getVideos变为异步。


同步变异步


因此,我们将getVideos重新改造为getVideosAsync,并返回一个Future包装:
// 异步查询视频列表方法
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;
}


这里,我们首先创建了异步计算的CompletableFuture对象,然后创建了一个新的线程去执行视频列表查询selectVideos,在查询完毕后会设置值到CompletableFuture;我们直接返回CompletableFuture对象,返回动作是独立于查询线程,因此没有被阻塞。

异步方式测试


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 毫秒后返回结果
我们可以发现,Video.getVideosAsync()在很短时间内就返回了,并没有阻塞当前方法的doSomethingOther。细心的读者可能发现,我们调用了videosAsync.get()方法,此方法意在获取异步包装对象中的值,属于阻塞方法,假如说我们的查询视频列表方法迟迟没有返回,它会永久阻塞在这里。因此我们开发中建议使用他的重载方法(V get(long timeout, TimeUnit unit)),传入超时时长,避免无限阻塞。
 
除此之外,另一个可能的情况是,查询列表方法报错了,我们就面临另一个问题:如何管理我们异步任务可能出现的错误,这在开发中是不容忽视的一个问题。


异步任务的错误处理


如果我们的异步视频列表查询方法报错,这个错误会被限制在试图查询的异步线程中,并终结该线程。这依然会导致调用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)
到现在为止,我们已经学会使用CompletableFuture进行异步方法调用,看起来也很简单。但事实上CompletableFuture本身为开发者提供了大量方便优雅的工厂方法,直接使用这些方法,配合1.8的新特性,能够构建更加优雅的异步应用。
 
点个关注,关于CompletableFuture更好实践,参见异步编程实践第三话。


【推荐阅读