CompletableFuture我们可以在什么场景下使用?

3,254 阅读5分钟

前言

此刻的你正在上传自己的生活视频,但由于视频过大,于是开启一个新线程来进行上传,这样,你就可以处理其他事情,但是,你想查看上传完成没有,就不得不每隔几秒钟去检测。

public class CompletableFutureTest {
    private static ExecutorService executor = Executors.newSingleThreadExecutor();
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Callable<String> callable = () -> {
            int progress = 0;
            while (progress <= 100) {
                int i = new Random().nextInt(10);
                progress += i;
                System.out.println(progress + "%");
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return "http://www.xxxx.mp4";
        };
        Future<?> submit = executor.submit(callable);
        while (!submit.isDone()) {
//            没有完成,做其他事
        }
        System.out.println("上传完成" + submit.get());
    }
}

后来你发现,这种做法非常麻烦、效率低下,于是开始探索更加方便的做法,最后你发现了CompletableFuture,CompletableFuture解决了Future不能解决的事,他可以异步回调,当任务完成后,会主动通知你,再也不需要你主动询问,从此,一切变的都轻而易举。

public class CompletableFutureTest {
    public static void main(String[] args) {
        CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
            int progress = 0;
            while (progress <= 100) {
                int i = new Random().nextInt(10);
                progress += i;
                System.out.println(progress + "%");
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return "http://www.xxxx.mp4";
        });
        completableFuture.thenAccept(s -> System.out.println("上传完成"+s));
        while (!completableFuture.isDone()){
        }
    }
}

CompletableFuture 简介

那么到底什么是CompletableFuture呢?

首先要了解下异步编程,异步编程是一种在与主线程不同的线程上运行任务,并且他能够通知主线程进度、完成或失败,用它来编写非阻塞代码的方法。这样,主线程不会阻塞/等待任务完成,并且可以并行执行其他任务,拥有这种并行性极大地提高了程序的性能,所以,CompletableFuture就是用于Java中的异步编程。

CompletableFuture的出现,就是将以更优雅的方式解决异步问题。

supplyAsync、runAsync

使用 supplyAsync()方法构建CompletableFuture,它表示这个异步任务有返回值,如果没有返回值,可以使用runAsync()方法,就像下面这样:

 CompletableFuture
         .runAsync(() -> System.out.println(Thread.currentThread().getName()))
         .thenAccept(unused -> System.out.println(Thread.currentThread().getName()+"完成"));

//输出
ForkJoinPool.commonPool-worker-1
main完成

既然有返回值,那么必定会有一个方法来获取,同Future一样,也是get()

CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> "string");

System.out.println(completableFuture.get());

thenAccept

当在任务结束后,需要拿到他的返回值进行处理,在CompletableFuture中可以借助thenAccept,他的返回值还是CompletableFuture,多次调用,会依次被回调。

 CompletableFuture
         .runAsync(() -> System.out.println("处理任务"+Thread.currentThread().getName()))
         .thenAccept(unused -> System.out.println("完成1"))
         .thenAccept(unused -> System.out.println("完成2"))
         .thenAccept(unused -> System.out.println("完成3"))
         .thenAccept(unused -> System.out.println("完成4"));

thenAccept()可以访问CompletableFuture的结果,还有一个方法thenRun()无法访问,thenRun()是在异步任务执行后,以同步的方式调用Runnable。

thenAccept()方法也就是以同步的方式消费上一个异步任务的结果。

将两个 CompletableFuture组合在一起(链式执行)

当一个需求需要两个接口来完成时,我们可以将他们组合在一起,比如,要获取与用户A相同爱好的用户信息列表,首先需要从A服务器获取用户A的爱好,然后从B服务器获取相同爱好的用户,此时第二阶段需要第一阶段的值。


public class Move {
public static void main(String[] args) {

    CompletableFuture<CompletableFuture<List<String>>> result = getUsersHobby("userId")
            .thenApply(s -> getHobbyList(s));
    try {
        System.out.println(result.get().get());
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
}

static CompletableFuture<String> getUsersHobby(String userId) {
    return CompletableFuture.supplyAsync(() -> {
        //发起网络请求,获取userId的爱好
        return "篮球";
    });
}

static CompletableFuture<List<String>> getHobbyList(String hobby) {
    //hobby是getUsersHobby()异步处理的结果
    return CompletableFuture.supplyAsync(() -> {
        //发起网络请求,获取具有爱好是hobby的所有用户
        return Arrays.asList("张三", "李四");
    });
}
}

使用thenApply() 方法导致了结果嵌套,我们不得不调用两次get来获取最终值,但如果希望最终结果是顶级 Future,那么可以改用thenCompose()方法

public static void main(String[] args) {
    CompletableFuture<List<String>> result = getUsersHobby("userId")
            .thenCompose(s -> getHobbyList(s));
    try {
        System.out.println(result.get());
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
}

thenApply方法也就是以同步的方式继续处理上一个异步任务的结果,当然还有thenApplyAsync()方法以异步方式处理上一个异步任务。

在两个CompletableFuture运行后再次计算

这种场景适用于当A和B通过异步取到值后,使用A、B计算出C,比如,获取两个用户的爱好交集,首先需要获取A用户的爱好,接着获取B用户的爱好,然后在求交集。


 public static void main(String[] args) throws ExecutionException, InterruptedException {
     CompletableFuture<List<String>> aUser = CompletableFuture.supplyAsync(() -> {
         return Stream.of("篮球","羽毛球").collect(Collectors.toList());
     });
     CompletableFuture<List<String>> bUser = CompletableFuture.supplyAsync(() -> {
         return Stream.of("篮球","排球").collect(Collectors.toList());
     });
     CompletableFuture<List<String>> combinedFuture = aUser
             .thenCombine(bUser, (aHobby, bHobby) -> {
                 aHobby.retainAll(bHobby);
                 return new ArrayList<>(aHobby);
             });
     System.out.println( combinedFuture.get());
 }

还有一个方式是runAfterBoth(),他在两个任务完成后,分别以同步/异步的方式执行Runnable。

 public CompletableFuture<Void> runAfterBoth(CompletionStage<?> other,
                                              Runnable action)
                                              
 public CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other,
                                                   Runnable action)

将多个 CompletableFuture 组合在一起

CompletableFuture.allOf 适用于在执行完众多异步任务后做某事时使用,问题allOf()在于它返回CompletableFuture<Void>,表示没有结果集, 但是我们可以通过编写一些额外的代码行来获得所有包装的 CompletableFutures的结果


 public static void main(String[] args) throws ExecutionException, InterruptedException {
     CompletableFuture<Void> completableFuture = CompletableFuture.allOf(getUserName("1"), 
             getUserName("2"),
             getUserName("3")).thenAccept(new Consumer<Void>() {
         @Override
         public void accept(Void unused) {
             System.out.println("完成");
         }
     });
     completableFuture.join();
 }
 static CompletableFuture<String> getUserName(String userId) {
     return CompletableFuture.supplyAsync(() -> {
         try {
             Thread.sleep(new Random().nextInt(1000));
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         return "用户-" + userId;
     });
 }

如果想运行一批任务中的其中一个,可以使用anyOf()方法,他会在其中挑一个进行执行。

CompletableFuture 异常处理

程序运行避免不了错误,那么CompletableFuture是如何处理异常的呢,先看以下代码,如果在supplyAsync()任务中发生错误,那么就不会调用任何thenApply()的回调,如果在第一个thenApply()回调中发生错误,则不会调用第二个及以后的,依此类推。

CompletableFuture.supplyAsync(() -> {
	// Code which might throw an exception
	return "Some result";
}).thenApply(result -> {
	return "processed result";
}).thenApply(result -> {
	return "result after further processing";
}).thenAccept(result -> {
	// do something with the final result
});

使用exceptionly()回调处理异常

如果程序要捕获异步任务过程中遇到的错误,可以在exceptionally中处理,并且返回一个默认值。

public static void main(String[] args) throws ExecutionException, InterruptedException {
    CompletableFuture<Integer> maturityFuture = CompletableFuture.supplyAsync(() -> {
        return  1/0;
    }).exceptionally(ex -> {
        ex.printStackTrace();
       return 1;
    });
    System.out.println(maturityFuture.get());
}