CompletableFuture使用详解

Future的缺点

  1. 不能手动完成 当你写了一个函数,用于通过一个远程API获取一个电子商务产品最新价格。因为这个 API 太耗时,你把它允许在一个独立的线程中,并且从你的函数中返回一个 Future。现在假设这个API服务宕机了,这时你想通过该产品的最新缓存价格手工完成这个Future 。你会发现无法这样做。
  2. Future 的结果在非阻塞的情况下,不能执行更进一步的操作。 Future 不会通知你它已经完成了,它提供了一个阻塞的 get() 方法通知你结果。你无法给 Future 植入一个回调函数,当 Future 结果可用的时候,用该回调函数自动的调用 Future 的结果。
  3. 多个 Future 不能串联在一起组成链式调用 有时候你需要执行一个长时间运行的计算任务,并且当计算任务完成的时候,你需要把它的计算结果发送给另外一个长时间运行的计算任务等等。你会发现你无法使用 Future 创建这样的一个工作流。
  4. 不能组合多个 Future 的结果 假设你有10个不同的Future,你想并行的运行,然后在它们运行未完成后运行一些函数。你会发现你也无法使用 Future 这样做。

CompletableFuture的创建

new CompletableFuture

通过new关键字直接创建CompletableFuture

测试案例1

csharp 复制代码
@Test
public void test() throws ExecutionException, InterruptedException {
    CompletableFuture<Object> completableFuture = new CompletableFuture<>();
    System.out.println("开始执行业务代码");
    Object o = completableFuture.get();
    System.out.println("业务代码执行完毕,返回结果:" + o);
}

调用CompletableFuture的get方法会获取异步任务的返回值,但是在测试案例1 中代码会阻塞在get方法中,因为我们没有给异步任务任何逻辑,它永远不会完成。我们可以complete方法来手动完成一个CompletableFuture。代码如下

测试案例2

java 复制代码
@Test
public void test1() throws ExecutionException, InterruptedException {
    CompletableFuture<Object> completableFuture = new CompletableFuture<>();
    System.out.println("开始执行业务代码");
    completableFuture.complete("完成这个任务!");
    Object o = completableFuture.get();
    System.out.println("业务代码执行完毕,返回结果:" + o);
}

测试案例2会的get会得到返回值。并且程序能够正常执行完毕,不会阻塞在get方法处。

静态方法completedFuture

java 复制代码
CompletableFuture future = CompletableFuture.completedFuture(1);

runAsync

runAsync方法创建不带返回值CompletableFuture

java 复制代码
@Test
public void test2() throws ExecutionException, InterruptedException {
    CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(() -> {
        System.out.println("开始执行任务");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("任务执行完毕");
    });
    completableFuture.get();
    System.out.println("主线程运行完毕");
}

我们通过静态方法runAsync来创建一个CompletableFuture,该方法的参数接收一个Runnable对象,也就是我们的异步任务。这才是平时用的最多的API来创建CompletableFuture。不过大家也注意到了,使用runAsync是执行异步任务是没有返回值的,那我们想通过异步任务获取返回值该怎么办呢?下面来说创建带返回值的异步任务

supplyAsync

supplyAsync方法创建带返回值的CompletableFuture

测试案例4

java 复制代码
    @Test
    public void test3() throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println("开始执行任务");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("任务执行完毕");
            return 1;
        });
        Integer integer = completableFuture.get();
        System.out.println("主线程运行完毕,异步任务返回结果:" + integer);
    }

supplyAsync接收一个Callable对象,该对象与Runnable不同的是就是可以在执行完异步任务后有一个返回值。这个返回值可以通过CompletableFuture的get方法获取到。测试案例4的输出结果如下

Plain 复制代码
开始执行任务
任务执行完毕
主线程运行完毕,异步任务返回结果:1

补充1:执行异步方法join()和get()

在前面创建CompletableFuture的例子中,无论异步任务是否有返回值,我们都是通过get方法来执行异步任务。但是get方法会强制要求我们捕获两个异常ExecutionException, InterruptedException,如果使用try...catch捕获会让代码看起来很不美观,并且有可能让代码多出好几行的赋值变量。还好CompletableFuture提供了另一个方法join可以替代get方法。它不要求开发者显示的捕获异常,而是内部把异常信息封装成了一个运行时异常------CompletionException。这样就无需我们手动处理异常了。

补充2:runAsync和supplyAsync的重载

上面说的runAsync和supplyAsync分别接收Runnable对象和Callable对象作为参数。其实这两个方法各自都还有另一个版本的重载方法。如下

java 复制代码
public static CompletableFuture<Void> runAsync(Runnable runnable)
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)

他们的重载方法都是多了一个Executor参数,意思其实就是让异步任务在我们自己定义的线程池中执行。实际开发中,每个项目都有自己的线程池,我们只需要把项目的线程池传进去就可以了。

那如果不传Executor这个参数,CompletableFuture是用了哪个线程执行的异步任务呢?

答案是ForkJoinPool#commonPool(),如果我们不显示的指定线程池,CompletableFuture底层就会默认采用Java7提供的ForkJoinPool线程池来处理异步任务。它依赖于系统的配置来决定它的大小System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "8");

CompletableFuture转变处理

thenApply、thenAccept和thenRun

thenApply

上面介绍了如何创建CompletableFuture,如何让CompletableFuture执行异步任务,获取异步任务的返回值。接下来就来说一下如何处理异步任务的返回值

假设我们现在有一个耗时方法是去远程服务器上检索大量数据,最后获取一个张图片的名称(不包括后缀),现在我们想在获取到图片名称之后为图片拼接上.jpg后缀。该怎么做呢?我们可以使用CompletableFuture提供的thenApply方法来处理异步任务的返回值。需求实现如下

java 复制代码
@Test
public void test() {
    CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
        // 模拟耗时操作
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return "picture_name";
    });
    CompletableFuture<String> stringCompletableFuture = completableFuture.thenApply(res -> {
        return res + ".jpg";
    });
    String result = stringCompletableFuture.join();
    System.out.println("主线程执行完毕,异步线程执行结果:" + result);
}

输出结果就是picture_name.jpg。thenApply接收一个Function的参数,把上一步执行的结果作为thenApply的参数,然后在thenApply内部就可以对上一步的返回结果进行处理了。这里可能会有同学问,我明明可以在return "picture_name";这个语句的返回值上拼接后缀干嘛还要使用thenApply方法呢?没错,直接拼接确实可以满足需求,达到最终效果。不过实际情况往往是处理异步返回的结果逻辑很复杂,如果都在supplyAsyncrunAsync方法中一步处理到位,会让代码变得很臃肿,代码越多就越不容易被复用、越不容易被维护。也就是说,如果只是一个简单的拼接字符串操作,完全可以在supplyAsyncrunAsync方法中完成;如果两段逻辑相互独立且很复杂,还是建议分开处理的好。

thenAccept和thenRun

thenAccept和thenRun和supplyAsync方法的使用是一模一样的,区别在于thenAccept接收上一步的异步结果作为参数,但没有返回值。thenRun则不能获取上一步的返回值,也不能有返回值。

补充:thenApply、thenAccept和thenRun的Async版本

thenApply、thenAccept和thenRun分别对应三个async的版本,异步执行。如果不使用Async版本的API,则后续执行逻辑会和前置CompletableFuture共用一个线程。如果使用Async版本API,则在新的线程池中执行。

  • thenApplyAsync

  • thenAcceptAsync

  • thenRunAsync

java 复制代码
public CompletableFuture<Void> thenRun(Runnable action)
public CompletableFuture<Void> thenRunAsync(Runnable action)
public CompletableFuture<Void> thenRunAsync(Runnable action, Executor executor)
public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor) 
public CompletableFuture<Void> thenAccept(Consumer<? super T> action)
public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action)
public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action, Executor executor)

CompletableFuture组合两个

thenCompose

假设我们有两个CompletableFuture

java 复制代码
    public static CompletableFuture<Object> getUser() {
        return CompletableFuture.completedFuture("username1");
    }

    public static CompletableFuture<String> getOrderByUser(Object user) {
        return CompletableFuture.completedFuture(user + "'s order.");
    }

getUser用来异步获取用户

getOrderByUser异步获取订单,但需要依赖用户。也就是要把getUser返回的用户作为参数传之。此时你可能会想到使用thenApply,就像下面这样

java 复制代码
    @Test
    public void test() {
        CompletableFuture<Object> user = getUser();
        user.thenApply(res -> {
            return getOrderByUser(user);
        });
    }

但很可惜,因为上面的user对象实际上是一个被CompletableFuture包装过的。使用thenApply实际上的返回值被嵌套了一层,真正的返回值如下

java 复制代码
    CompletableFuture<CompletableFuture<String>> result = user.thenApply(res -> {
        return getOrderByUser(user);
    });

那我们应该怎么组合两个CompletableFuture呢?

CompletableFuture提供了一个APIthenCompose可以组合两个CompletableFuture,把一个CompletableFuture的返回值作为参数传递给另一个CompletableFuture

java 复制代码
     CompletableFuture<Object> order = getUser().thenCompose(o -> getOrderByUser(o));

thenCombine

假设现在有两个异步任务future1和future2,我们想要等future1和future2都执行完后,对他们的结果进行处理。这时就需要使用thenCombine来绑定两个CompletableFuture。如下

java 复制代码
    @Test
    public void test2() {
        CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
            return 1;
        });
        CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
            return 2;
        });
        CompletableFuture<Integer> bothFuture = future1.thenCombine(future2, (res1, res2) -> {
            return res1 + res2;
        });
        System.out.println(bothFuture.join());
    }

thenCompose与thenCombine的区别

thenCompose是一个异步任务依赖于另一个异步任务的返回值。相当于是串行的。

thenCombine是等待两个异步任务都完成后对其结果进行操作。该行为是是并发的。

thenComposeAsync与thenCombineAsync

CompletableFuture中async结尾的都是可以放在自定义的线程池中执行的,后面不再赘述

java 复制代码
    public <U,V> CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn)
    public <U,V> CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn, Executor executor)
    public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn)
    public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn, Executor executor)

CompletableFuture组合多个

前面说到组合两个CompletableFuture操作,那么如何组合多个CompletableFuture呢?比如现在有100个异步任务,需要在100个异步任务都执行完毕后做某件事,怎么办呢?又或者100个异步任务我只需要任意一个异步任务执行完就做某些业务逻辑处理,该怎么实现?

allOf

allOf可以组合任意多个CompletableFuture,他的作用是等待所有的CompletableFuture都完成。用法如下:

java 复制代码
    CompletableFuture[] futures = new CompletableFuture[]{future1, future2, future3};
    CompletableFuture<Void> bothFuture = CompletableFuture.allOf(futures);
    bothFuture.join();

那么当我们有3个异步任务,想要等待3个异步任务都执行完后再做某些事,就可以使用allOf方法。

java 复制代码
    @Test
    public void test3() {
        System.out.println("开始");
        LocalDateTime start = LocalDateTime.now();
        CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            return 1;
        });

        CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            return 2;
        });
        CompletableFuture<Integer> future3 = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            return 3;
        });
        CompletableFuture[] futures = new CompletableFuture[]{future1, future2, future3};
        CompletableFuture<Void> bothFuture = CompletableFuture.allOf(futures);
        bothFuture.join();
        LocalDateTime end = LocalDateTime.now();
        Duration between = Duration.between(start, end);
        System.out.println("耗时:" + between.get(ChronoUnit.SECONDS));
    }

anyOf

用法同allOf,语义与之不同,它表示任意一个异步任务执行完毕都会返回。且最先执行完毕的任务会被作为返回值返回

java 复制代码
    @Test
    public void test4() throws ExecutionException, InterruptedException {
        System.out.println("开始");
        LocalDateTime start = LocalDateTime.now();
        CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(4000);
                System.out.println("future1执行完毕");

            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            return 1;
        });

        CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            return 2;
        });
        CompletableFuture<Integer> future3 = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(3000);
                System.out.println("future3执行完毕");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            return 3;
        });
        CompletableFuture[] futures = new CompletableFuture[]{future1, future2, future3};
        CompletableFuture<Object> anyOf = CompletableFuture.anyOf(futures);
        anyOf.join();
        LocalDateTime end = LocalDateTime.now();
        Duration between = Duration.between(start, end);
        System.out.println("耗时:" + anyOf.join() + "-" + between.get(ChronoUnit.SECONDS));
        CompletableFuture future = new CompletableFuture();
        future.get();
    }

上面的案例中,future2耗时只有2秒,所以anyOf的返回值就会是future2,最后输出的anyOf.join()结果自然是2。

CompletableFuture异常处理

如果在执行CompletableFuture的时候发生了异常,我们又该怎么处理。CompletableFuture提供了2个API来处理异常,exceptionallyhandler

exceptionally

java 复制代码
    @Test
    public void test() {
        CompletableFuture<Object> future1 = CompletableFuture.supplyAsync(() -> {
            throw new RuntimeException("future1 exception");
        }).exceptionally(exception -> {
            System.out.println("程序发生异常");
            return "hello";
        });
        System.out.println("执行结果1" +future1.join());
    }

执行结果:

程序发生异常
执行结果1hello

需要注意的是exceptionally必须要链式调用,因为它返回的是另一个CompletableFuture对象,而不是对原来的CompletableFuture进行操作,如果不使用链式调用,还是会抛出异常

exceptionally反例:

java 复制代码
    @Test
    public void test() {
        CompletableFuture<Object> future1 = CompletableFuture.supplyAsync(() -> {
            throw new RuntimeException("future1 exception");
        });
        future1.exceptionally(exception -> {
            System.out.println("程序发生异常");
            return "hello";
        });
        System.out.println("执行结果1" + future1.join());
    }

输出结果:

handler

java 复制代码
    @Test
    public void test2() throws ExecutionException, InterruptedException {
        CompletableFuture<Object> future1 = CompletableFuture.supplyAsync(() -> {
            throw new RuntimeException("future1 exception");
        }).handle((result, exception) -> {
            if (exception != null) {
                System.out.println("程序发生异常");
                // 其他异常处理步骤
            } else {
                System.out.println("异步任务执行结果:" + result);
            }
            return "自定义返回结果";
        });
        System.out.println(future1.join());
    }

输出结果

程序发生异常
自定义返回结果
相关推荐
AI人H哥会Java2 分钟前
【Spring】基于XML的Spring容器配置——<bean>标签与属性解析
java·开发语言·spring boot·后端·架构
计算机学长felix5 分钟前
基于SpringBoot的“大学生社团活动平台”的设计与实现(源码+数据库+文档+PPT)
数据库·spring boot·后端
sin22015 分钟前
springboot数据校验报错
spring boot·后端·python
开心工作室_kaic11 分钟前
springboot493基于java的美食信息推荐系统的设计与实现(论文+源码)_kaic
java·开发语言·美食
缺少动力的火车13 分钟前
Java前端基础—HTML
java·前端·html
loop lee21 分钟前
Redis - Token & JWT 概念解析及双token实现分布式session存储实战
java·redis
ThetaarSofVenice22 分钟前
能省一点是一点 - 享元模式(Flyweight Pattern)
java·设计模式·享元模式
InSighT__24 分钟前
设计模式与游戏完美开发(2)
java·游戏·设计模式
神仙别闹24 分钟前
基于Java2D和Java3D实现的(GUI)图形编辑系统
java·开发语言·3d
dbcat官方29 分钟前
1.微服务灰度发布(方案设计)
java·数据库·分布式·微服务·中间件·架构