任务编排框架: CompletableFuture

484 阅读23分钟

CompletableFuture异步回调

详解

CompletableFuture实现了Future和CompletionStage两个接口。该类的实例作为一个异步任务,可以在自己异步执行完成之后触发一些其他的异步任务,从而达到异步回调的效果。

public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {}

CompletionStage代表异步计算过程中的某一个阶段,一个阶段完成以后可能会进入另一个阶段。一个阶段可以理解为一个子任务,每一个子任务会包装一个Java函数式接口实例,表示该子任务所要执行的操作。

CompletionStage

CompletionStage代表某个同步或者异步计算的一个阶段,或者一系列异步任务中的一个子任务(或者阶段性任务)。

每个CompletionStage子任务所包装的可以是一个Function、Consumer或者Runnable函数式接口实例。这三个常用的函数式接口的特点如下:

(1)FunctionFunction接口的特点是:有输入、有输出。包装了Function实例的CompletionStage子任务需要一个输入参数,并会产生一个输出结果到下一步。

(2)RunnableRunnable接口的特点是:无输入、无输出。包装了Runnable实例的CompletionStage子任务既不需要任何输入参数,又不会产生任何输出。

(3)ConsumerConsumer接口的特点是:有输入、无输出。包装了Consumer实例的CompletionStage子任务需要一个输入参数,但不会产生任何输出。

多个CompletionStage构成了一条任务流水线,一个环节执行完成了可以将结果移交给下一个环节(子任务)。多个CompletionStage子任务之间可以使用链式调用,下面是一个简单的例子:

@Test
public void test1() throws InterruptedException {
    CompletableFuture.supplyAsync(() -> "Hello")
        .thenApply(x -> x + "==" + x)
        .thenAccept(x -> System.out.println(x))
        .thenRun(() -> System.out.println("END"))
        .join();
}
  1. CompletableFuture.supplyAsync(() -> "Hello")

    提供了一个任务,这是一个前提

  2. thenApply(x -> x + "==" + x)

    thenApply将一个Function类型的Lambda表达式包装成一个子任务,接受一个参数,返回一个值

  3. thenAccept(x -> System.out.println(x))

    thenAccept将一个Consumer类型的Lambda表达式包装成一个子任务,接受一个参数,但是不返回值

  4. thenRun(() -> System.out.println("END"))

    thenRun将一个Runnable类型的Lambda表达式包装成一个子任务,既不接收参数,也不返回值

  5. join()

    等待执行完成并返回结果,注意join只能用于已经完成的CompletableFuture,不然会导致当前线程永久阻塞,这里的已完成主要指的是supplyAsync中的任务

CompletionStage代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另一个阶段。虽然一个子任务可以触发其他子任务,但是并不能保证后续子任务的执行顺序。

创建子任务

CompletionStage子任务的创建是通过CompletableFuture完成的。CompletableFuture类提供了非常强大的Future的扩展功能来帮助我们简化异步编程的复杂性,提供了函数式编程的能力来帮助我们通过回调的方式处理计算结果,也提供了转换和组合CompletionStage()的方法。CompletableFuture定义了一组方法用于创建CompletionStage子任务(或者阶段性任务)。

在使用CompletableFuture创建CompletionStage子任务时,如果没有指定Executor线程池,在默认情况下CompletionStage会使用公共的ForkJoinPool线程池。

示例如下:

public class Cf2Test {

    @Test
    public void runAsync() {
        // join 的含义可参考上文
        CompletableFuture.runAsync(() -> System.out.println(LocalDateTime.now()))
                .join();
    }

    @Test
    public void runAsyncExecutors() {
        // join 的含义可参考上文
        ExecutorService pool = Executors.newFixedThreadPool(1);
        CompletableFuture.runAsync(() -> System.out.println(LocalDateTime.now()), pool)
                .join();
    }

    @Test
    public void supplyAsync() {
        // join 的含义可参考上文
        LocalDateTime time = CompletableFuture.supplyAsync(LocalDateTime::now)
                .join();
        System.out.println("time = " + time);
    }

    @Test
    public void supplyAsyncExecutors() {
        // join 的含义可参考上文
        ExecutorService pool = Executors.newFixedThreadPool(1);
        LocalDateTime time = CompletableFuture.supplyAsync(LocalDateTime::now, pool)
                .join();
        System.out.println("time = " + time);
    }
}

设置子任务回调钩子

可以为CompletionStage子任务设置特定的回调钩子,当计算结果完成或者抛出异常的时候,执行这些特定的回调钩子。

调用cancel()方法取消CompletableFuture时,任务被视为异常完成,completeExceptionally()方法所设置的异常回调钩子也会被执行。

//设置子任务完成时的回调钩子
public CompletableFuture<T> whenComplete(BiConsumer<? super T,? super Throwable> action);

//设置子任务完成时的回调钩子,可能不在同一线程执行
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action);

//设置子任务完成时的回调钩子,提交给线程池executor执行
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action,Executor executor);

//设置异常处理的回调钩子
public CompletableFuture<T> exceptionally(Function<Throwable,? extends T> fn);

注意:不要在 whenComplete 后面直接调用 exceptionally ,否则会执行异常钩子。示例如下:

@Test
public void hook() {
    CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello CompletableFuture");
    future.whenComplete((v, e) -> {
        System.out.println("whenComplete v = " + v);
        System.out.println("whenComplete e = " + e.getMessage());
    });
    future.exceptionally(t -> {
        System.out.println("exceptionally: " + t.getMessage());
        return t.getMessage();
    });
    String s = future.join();
    System.out.println("s = " + s);
}

handle统一处理结果和异常

除了分别通过whenComplete、exceptionally设置完成钩子、异常钩子之外,还可以调用handle()方法统一处理结果和异常。

//在执行任务的同一个线程中处理异常和结果
public <U> CompletionStage<U> handle(BiFunction<? super T, Throwable, ? extends U> fn);

//可能不在执行任务的同一个线程中处理异常和结果
public <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn);

//在指定线程池executor中处理异常和结果
public <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn,  Executor executor);

示例如下:

@Test
public void handle() {
    CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello");

    CompletableFuture<String> f = future.handle((v, t) -> {
        if (Objects.nonNull(t)) {
            System.out.println("发生了异常");
            return "ERROR";
        } else {
            System.out.println("v = " + v);
            return "SUCCESS";
        }
    });
    System.out.println(future.join());
    System.out.println(f.join());
}

线程池的使用

默认情况下,通过静态方法runAsync()、supplyAsync()创建的CompletableFuture任务会使用公共的ForkJoinPool线程池,默认的线程数是CPU的核数。

问题是,如果所有CompletableFuture共享一个线程池,那么一旦有任务执行一些很慢的IO操作,就会导致线程池中的所有线程都阻塞在IO操作上,造成线程饥饿,进而影响整个系统的性能。所以,强烈建议大家根据不同的业务类型创建不同的线程池,以避免互相干扰。

自定义线程池,示例如下:

@Test
public void test2() {
    int processors = Runtime.getRuntime().availableProcessors();
    ThreadPoolExecutor pool = new ThreadPoolExecutor(
        processors,
        processors * 2,
        1, TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(processors * 10),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.CallerRunsPolicy()
    );
    String s = CompletableFuture.supplyAsync(() -> "Hello", pool)
        .join();
    System.out.println("s = " + s);
}

异步任务执行

串行

如果两个异步任务需要串行(一个任务依赖另一个任务)执行,可以通过CompletionStage接口的thenApply()、thenAccept()、thenRun()和thenCompose()四个方法来实现。

thenApply

接受上一个任务的返回结果,当前任务进行处理,返回一个指定类型。thenApply()方法适用于在CompletableFuture完成后对结果进行转换或处理的场景。它提供了一种简洁的方式来处理异步任务链中的中间步骤,或者对异步任务的结果进行转换。

具体应用场景如下:

  1. 异步任务链的中间步骤:当我们有一系列的异步任务链,并且需要在中间某个步骤对CompletableFuture的结果进行转换或处理时,可以使用thenApply()方法。例如,对结果进行转换、过滤、映射等操作。
  2. 异步任务的转换器:当我们需要在某个异步任务完成后对结果进行转换时,可以使用thenApply()方法。例如,将结果从一种类型转换为另一种类型。
  3. 异步任务的串联:当我们需要在某个异步任务完成后触发另一个任务的执行,并且需要使用前一个任务的结果进行处理时,可以使用thenApply()方法。

thenApply有三个重载的版本,声明如下:

public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn) {
    return uniApplyStage(null, fn);
}

public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn) {
    return uniApplyStage(asyncPool, fn);
}

public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor) {
    return uniApplyStage(screenExecutor(executor), fn);
}

fn的类型声明涉及两个泛型参数,具体如下:

T 上一个任务返回的结果, U 当前任务返回的类型。

随机值a,当a大于等于5时乘以2,当a小于5时变为0,示例如下:

@Test
public void thenApply() {
    Integer integer = CompletableFuture.supplyAsync(() -> {
        int i = ThreadLocalRandom.current().nextInt(10);
        System.out.println("producer: " + i);
        return i;
    })
        .thenApply(n -> {
            if (n >= 5) {
                return n * 2;
            } else {
                return 0;
            }
        }).join();
    System.out.println("integer = " + integer);
}

thenAccept

接收前一个任务的返回结果,但是没有返回值。thenAccept()方法适用于在CompletableFuture完成后对结果进行处理或消费的场景。它提供了一种简洁的方式来处理异步任务链中的中间步骤,或者对异步任务的结果进行消费。

具体应用场景如下:

  1. 异步任务链的中间步骤:当我们有一系列的异步任务链,并且需要在中间某个步骤处理CompletableFuture的结果时,可以使用thenAccept()方法。例如,对结果进行转换、打印日志等操作。
  2. 异步任务的消费者:当我们需要在某个异步任务完成后对结果进行消费时,可以使用thenAccept()方法。例如,将结果写入数据库、发送消息等操作。
  3. 异步任务的回调:当我们需要在某个异步任务完成后触发另一个任务的执行,并且需要使用前一个任务的结果时,可以使用thenAccept()方法。

三个重载版本声明如下:

public CompletableFuture<Void> thenAccept(Consumer<? super T> action) {
    return uniAcceptStage(null, action);
}

public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action) {
    return uniAcceptStage(asyncPool, action);
}

public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action,Executor executor) {
    return uniAcceptStage(screenExecutor(executor), action);
}
@Test
public void thenAccept() {
    CompletableFuture.supplyAsync(() -> "Hello")
        .thenAccept((v) -> System.out.println(v)).join();
}

thenRun

既不能接受上一个任务的参数,也不支持返回值。适合独立任务。thenRun()方法适用于在CompletableFuture完成后执行一些操作,而不需要使用CompletableFuture的结果。它提供了一种简洁的方式来处理异步任务链的最后一步或者执行一些清理工作。

具体应用场景如下:

  1. 异步任务链的最后一步:当我们有一系列的异步任务链,但在最后一步不需要处理结果,只需要执行某些操作或者触发其他操作时,可以使用thenRun()方法。
  2. 异步任务链的清理工作:当我们需要在异步任务链中的某个步骤完成后执行一些清理工作,例如关闭资源、释放锁等,可以使用thenRun()方法。
  3. 异步任务的触发器:当我们需要在某个异步任务完成后触发另一个任务的执行,而不关心前一个任务的结果时,可以使用thenRun()方法。

thenRun有三个重载的版本,声明如下:

public CompletableFuture<Void> thenRun(Runnable action) {
    return uniRunStage(null, action);
}

public CompletableFuture<Void> thenRunAsync(Runnable action) {
    return uniRunStage(asyncPool, action);
}

public CompletableFuture<Void> thenRunAsync(Runnable action,Executor executor) {
    return uniRunStage(screenExecutor(executor), action);
}
@Test
public void thenRun() {
    CompletableFuture.supplyAsync(() -> "Hello")
        .thenRun(() -> System.out.println("thenRun")).join();
}

thenCompose

可以对两个任务进行串行的调度操作,第一个任务操作完成时,将它的结果作为参数传递给第二个任务。thenCompose()方法要求第二个任务的返回值是一个CompletionStage异步实例。因此,可以调用CompletableFuture.supplyAsync()方法将第二个任务所要调用的普通异步方法包装成一个CompletionStage异步实例。

thenCompose()方法适用于在CompletableFuture完成后触发另一个异步任务的执行,并且后续任务依赖前一个任务的结果的场景。它提供了一种简洁的方式来处理异步任务链的链式调用、扁平化和嵌套。

具体应用场景如下:

  1. 异步任务的链式调用:当我们有一系列的异步任务链,并且需要在某个任务完成后触发另一个任务的执行,并且后续任务依赖前一个任务的结果时,可以使用thenCompose()方法。它能够将前一个任务的结果作为输入,执行后续任务,并将后续任务的结果作为整个任务链的结果。
  2. 异步任务的扁平化:当我们有一系列的异步任务链,并且需要将多个任务的结果合并为一个结果时,可以使用thenCompose()方法。例如,如果我们有多个异步任务,每个任务返回一个CompletableFuture,我们可以使用thenCompose()方法将这些CompletableFuture合并为一个CompletableFuture。
  3. 异步任务的嵌套:当我们需要在某个异步任务完成后触发另一个异步任务的执行,并且后续任务依赖前一个任务的结果时,可以使用thenCompose()方法。它能够将前一个任务的结果作为输入,执行后续任务,并返回一个新的CompletableFuture。

三个重载版本声明如下:

public <U> CompletableFuture<U> thenCompose(Function<? super T, ? extends CompletionStage<U>> fn) {
    return uniComposeStage(null, fn);
}

public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn) {
    return uniComposeStage(asyncPool, fn);
}

public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn,
                                                 Executor executor) {
    return uniComposeStage(screenExecutor(executor), fn);
}
@Test
public void thenCompose() {
    String s = CompletableFuture.supplyAsync(() -> "Hello")
        .thenCompose(v -> CompletableFuture.supplyAsync(() -> v + "==" + v))
        .join();
    System.out.println(s);
}

总结

thenApply()、thenRun()、thenAccept()这三个方法的不同之处主要在于其核心参数fn、action、consumer的类型不同,分别为Function<T,R>、Runnable、Consumer<? super T>类型。

但是,thenCompose()方法与thenApply()方法有本质的不同:

(1)thenCompose()的返回值是一个新的CompletionStage实例,可以持续用来进行下一轮CompletionStage任务的调度。具体来说,thenCompose()返回的是包装了普通异步方法的CompletionStage任务实例,通过该实例还可以进行下一轮CompletionStage任务的调度和执行,比如可以持续进行CompletionStage链式(或者流式)调用。

(2)thenApply()的返回值则简单多了,直接就是第二个任务的普通异步方法的执行结果,它的返回类型与第二步执行的普通异步方法的返回类型相同,通过thenApply()所返回的值不能进行下一轮CompletionStage链式(或者流式)调用。

合并

如果某个任务同时依赖另外两个异步任务的执行结果,就需要对另外两个异步任务进行合并。

对两个异步任务的合并可以通过CompletionStage接口的thenCombine()、runAfterBoth()、thenAcceptBoth()三个方法来实现。这三个方法的不同之处主要在于其核心参数fn、action、consumer的类型不同,分别为Function<T,R>、Runnable、Consumer<? super T>类型。

thenCombine

thenCombine()会在两个CompletionStage任务都执行完成后,把两个任务的结果一起交给thenCombine()来处理。

thenCombine()方法适用于在两个CompletableFuture都完成后执行一个任务,并将两个任务的结果合并为一个结果的场景。它提供了一种简洁的方式来处理异步任务的组合、并行处理和依赖关系。

具体应用场景如下:

  1. 异步任务的组合:当我们有两个独立的异步任务,并且需要在它们都完成后将它们的结果合并为一个结果时,可以使用thenCombine()方法。例如,我们可以将两个异步任务的结果进行加法、乘法、字符串拼接等操作。
  2. 异步任务的并行处理:当我们有两个独立的异步任务,并且不依赖彼此的结果时,可以使用thenCombine()方法来并行处理它们。这样可以提高执行效率,同时等待两个任务都完成后再进行下一步操作。
  3. 异步任务的依赖关系:当我们有两个异步任务,并且后续任务依赖这两个任务的结果时,可以使用thenCombine()方法。它能够将两个任务的结果作为输入,执行后续任务,并返回一个新的CompletableFuture。

三个重载版本声明如下:

public <U,V> CompletableFuture<V> thenCombine(CompletionStage<? extends U> other,
                                              BiFunction<? super T,? super U,? extends V> fn) {
    return biApplyStage(null, other, fn);
}

public <U,V> CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other,
                                                   BiFunction<? super T,? super U,? extends V> fn) {
    return biApplyStage(asyncPool, other, fn);
}

public <U,V> CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other,
                                                   BiFunction<? super T,? super U,? extends V> fn, Executor executor) {
    return biApplyStage(screenExecutor(executor), other, fn);
}

thenCombine第一个参数是一个新的CompletableFuture。第二个参数是一个BiFuntion,用来接收2个异步任务,其中第一个值为最初的异步任务,第二个值为thenCombine中的异步任务。

@Test
public void thenCombine() {
    Integer integer = CompletableFuture.supplyAsync(() -> 10)
        .thenCombine(CompletableFuture.supplyAsync(() -> 20),
                     (l, r) -> l + r)
        .join();
    System.out.println("integer = " + integer);
}

runAfterBoth

runAfterBoth()方法不关心每一步任务的输入参数和处理结果。

runAfterBoth()方法适用于在两个CompletableFuture都完成后执行一个任务,并不依赖CompletableFuture的结果的场景。它提供了一种简洁的方式来处理异步任务的并行处理、清理工作和后续操作。

具体应用场景如下:

  1. 异步任务的并行处理:当我们有两个独立的异步任务,并且不依赖彼此的结果时,可以使用runAfterBoth()方法来并行处理它们。这样可以提高执行效率,同时等待两个任务都完成后再进行下一步操作。
  2. 异步任务的清理工作:当我们有两个异步任务,并且在它们都完成后需要执行一些清理工作时,可以使用runAfterBoth()方法。例如,我们可以在两个任务都完成后,关闭资源、释放内存等。
  3. 异步任务的后续操作:当我们有两个异步任务,并且后续任务不依赖这两个任务的结果时,可以使用runAfterBoth()方法。它能够在两个任务都完成后执行后续任务,并返回一个新的CompletableFuture。

三个重载版本声明如下:

public CompletableFuture<Void> runAfterBoth(CompletionStage<?> other,Runnable action) {
    return biRunStage(null, other, action);
}

public CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other,Runnable action) {
    return biRunStage(asyncPool, other, action);
}

public CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other,Runnable action,Executor executor) {
    return biRunStage(screenExecutor(executor), other, action);
}
@Test
public void runAfterBoth() {
    CompletableFuture.supplyAsync(() -> 10)
        .runAfterBoth(CompletableFuture.supplyAsync(() -> 20),
                      () -> System.out.println("runAfterBoth"))
        .join();
}

thenAcceptBoth

接受两个任务的结果作为参数,但是没有返回值。

thenAcceptBoth()方法用于在两个CompletableFuture都完成后执行一个Consumer任务,该任务会使用两个CompletableFuture的结果作为输入,并进行消费操作,不返回任何结果。它返回一个新的CompletableFuture,该CompletableFuture在Consumer任务完成后完成,并将原始的两个CompletableFuture的结果作为自己的结果。

具体应用场景如下:

  1. 异步任务的合并操作:当我们有两个独立的异步任务,并且需要将它们的结果进行合并操作时,可以使用thenAcceptBoth()方法。例如,我们可以将两个任务的结果进行加法、乘法、字符串拼接等操作。
  2. 异步任务的消费操作:当我们有两个异步任务,并且需要对它们的结果进行消费操作时,可以使用thenAcceptBoth()方法。例如,我们可以将两个任务的结果进行打印、写入文件、发送消息等操作。
  3. 异步任务的依赖关系:当我们有两个异步任务,并且后续任务需要依赖这两个任务的结果时,可以使用thenAcceptBoth()方法。它能够将两个任务的结果作为输入,执行后续任务,并返回一个新的CompletableFuture。

三个重载版本声明如下:

public <U> CompletableFuture<Void> thenAcceptBoth(CompletionStage<? extends U> other,BiConsumer<? super T, ? super U> action) {
    return biAcceptStage(null, other, action);
}

public <U> CompletableFuture<Void> thenAcceptBothAsync(CompletionStage<? extends U> other,BiConsumer<? super T, ? super U> action) {
    return biAcceptStage(asyncPool, other, action);
}

public <U> CompletableFuture<Void> thenAcceptBothAsync(CompletionStage<? extends U> other,BiConsumer<? super T, ? super U> action, Executor executor) {
    return biAcceptStage(screenExecutor(executor), other, action);
}

thenAcceptBoth中的BiConsumer的两个参数。第一个表示原始的任务,第二个表示thenAcceptBoth中的任务。

@Test
public void thenAcceptBoth() {
    CompletableFuture.supplyAsync(() -> 10)
        .thenAcceptBoth(CompletableFuture.supplyAsync(() -> 20),
                        (l, r) -> {
                            System.out.println("l = " + l);
                            System.out.println("r = " + r);
                        })
        .join();
}

allOf

如果需要合并多个异步任务,那么可以调用allOf()。allOf()方法适用于等待多个CompletableFuture都完成后执行某个任务的场景。它提供了一种简洁的方式来处理异步任务的并行执行、聚合操作和依赖关系。

具体应用场景如下:

  1. 并行执行多个异步任务:当我们有多个独立的异步任务,并且希望并行执行它们,等待所有任务都完成后再进行下一步操作时,可以使用allOf()方法。这样可以提高执行效率,同时等待所有任务都完成后再进行后续操作。
  2. 异步任务的聚合操作:当我们有多个异步任务,并且需要将它们的结果进行聚合操作时,可以使用allOf()方法。例如,我们可以将多个任务的结果进行合并、计算总和、拼接字符串等操作。
  3. 异步任务的依赖关系:当我们有多个异步任务,并且后续任务依赖这些任务的结果时,可以使用allOf()方法。它能够等待所有任务都完成后执行后续任务,并返回一个新的CompletableFuture。

声明如下:

public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs) {
    return andTree(cfs, 0, cfs.length - 1);
}
@Test
public void allOf() {
    CompletableFuture.allOf(
        CompletableFuture.supplyAsync(() -> {
            System.out.println("10");
            return 10;
        }),
        CompletableFuture.supplyAsync(() -> {
            System.out.println("20");
            return 20;
        }), CompletableFuture.runAsync(() -> {
            System.out.println("30");
        })
    ).join();
}

选择

CompletableFuture对异步任务的选择执行不是按照某种条件进行选择的,而是按照执行速度进行选择的:前面两个并行任务,谁的结果返回速度快,谁的结果将作为第三步任务的输入。

对两个异步任务的选择可以通过CompletionStage接口的applyToEither()、runAfterEither()和acceptEither()三个方法来实现。这三个方法的不同之处在于它的核心参数fn、action、consumer的类型不同,分别为Function<T,R>、Runnable、Consumer<? super T>类型。

applyToEither

两个CompletionStage谁返回结果的速度快,applyToEither()方法就用这个最快的CompletionStage的结果进行下一步(第三步)的回调操作。

applyToEither()方法适用于在多个CompletableFuture中任意一个完成后执行一个任务,并返回一个新的CompletableFuture的场景。它提供了一种简洁的方式来处理异步任务的竞争操作、选择操作和超时处理。

具体应用场景如下:

  1. 异步任务的竞争操作:当我们有多个异步任务,并且只需要获取其中一个任务的结果时,可以使用applyToEither()方法。它能够等待多个任务中任意一个完成后执行一个任务,并返回一个新的CompletableFuture,该CompletableFuture的结果是任务的结果。
  2. 异步任务的选择操作:当我们有多个异步任务,并且需要根据条件选择其中一个任务的结果时,可以使用applyToEither()方法。例如,我们可以根据第一个任务的结果是否满足某个条件来选择返回该任务的结果,或者选择返回另一个任务的结果。
  3. 异步任务的超时处理:当我们有多个异步任务,并且需要在一定时间内获取其中一个任务的结果,可以使用applyToEither()方法结合CompletableFuture的工具方法来实现超时处理。例如,我们可以使用CompletableFuture的anyOf()方法等待多个任务中任意一个完成,然后使用applyToEither()方法获取完成的任务的结果。

三个重载版本声明如下:

public <U> CompletableFuture<U> applyToEither(CompletionStage<? extends T> other, Function<? super T, U> fn) {
    return orApplyStage(null, other, fn);
}

public <U> CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T, U> fn) {
    return orApplyStage(asyncPool, other, fn);
}

public <U> CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T, U> fn,
                                                   Executor executor) {
    return orApplyStage(screenExecutor(executor), other, fn);
}
@Test
public void applyToEither() {
    String s = CompletableFuture.supplyAsync(() -> {
        try {
            int timeout = ThreadLocalRandom.current().nextInt(5);
            System.out.println("A = " + timeout);
            TimeUnit.SECONDS.sleep(timeout);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("A");
        return "A";
    }).applyToEither(CompletableFuture.supplyAsync(() -> {
        try {
            int timeout = ThreadLocalRandom.current().nextInt(5);
            System.out.println("B = " + timeout);
            TimeUnit.SECONDS.sleep(timeout);
            System.out.println("B");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return "B";
    }), v -> v).join();
    System.out.println("s = " + s);
}

runAfterEither

runAfterEither()方法适用于在多个CompletableFuture中任意一个完成后执行一个Runnable任务的场景。它提供了一种简洁的方式来处理异步任务的竞争操作、清理操作和超时处理。

具体应用场景如下:

  1. 异步任务的竞争操作:当我们有多个异步任务,并且只需要在其中一个任务完成后执行一个任务,无需等待任务结果时,可以使用runAfterEither()方法。它能够等待多个任务中任意一个完成后执行一个任务,并返回一个新的CompletableFuture,该CompletableFuture的结果为null。
  2. 异步任务的清理操作:当我们有多个异步任务,并且需要在其中一个任务完成后执行一些清理操作时,可以使用runAfterEither()方法。例如,我们可以在一个任务完成后,清理一些资源或者取消其他任务的执行。
  3. 异步任务的超时处理:当我们有多个异步任务,并且需要在一定时间内获取其中一个任务的结果,可以使用runAfterEither()方法结合CompletableFuture的工具方法来实现超时处理。例如,我们可以使用CompletableFuture的anyOf()方法等待多个任务中任意一个完成,然后使用runAfterEither()方法执行超时处理的任务。

三个重载版本声明如下:

public CompletableFuture<Void> runAfterEither(CompletionStage<?> other,Runnable action) {
    return orRunStage(null, other, action);
}

public CompletableFuture<Void> runAfterEitherAsync(CompletionStage<?> other,Runnable action) {
    return orRunStage(asyncPool, other, action);
}

public CompletableFuture<Void> runAfterEitherAsync(CompletionStage<?> other,Runnable action,Executor executor) {
    return orRunStage(screenExecutor(executor), other, action);
}
@Test
public void runAfterEither() {
    CompletableFuture.supplyAsync(() -> {
        try {
            int timeout = ThreadLocalRandom.current().nextInt(5);
            System.out.println("A = " + timeout);
            TimeUnit.SECONDS.sleep(timeout);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("A");
        return "A";
    }).runAfterEither(CompletableFuture.supplyAsync(() -> {
        try {
            int timeout = ThreadLocalRandom.current().nextInt(5);
            System.out.println("B = " + timeout);
            TimeUnit.SECONDS.sleep(timeout);
            System.out.println("B");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return "B";
    }), () -> System.out.println("runAfterEither")).join();
}

acceptEither

acceptEither()方法适用于在多个CompletableFuture中任意一个完成后执行一个消费操作的场景。它提供了一种简洁的方式来处理异步任务的竞争操作、并行处理和超时处理。

具体应用场景如下:

  1. 异步任务的竞争操作:当我们有多个异步任务,并且只需要在其中一个任务完成后执行一个消费操作,无需等待任务结果时,可以使用acceptEither()方法。它能够等待多个任务中任意一个完成后执行一个消费操作。
  2. 异步任务的并行处理:当我们有多个异步任务,并且需要对任务的结果进行消费操作时,可以使用acceptEither()方法。例如,我们可以将多个任务的结果进行合并、计算总和、打印日志等操作。
  3. 异步任务的超时处理:当我们有多个异步任务,并且需要在一定时间内获取其中一个任务的结果,并进行消费操作,可以使用acceptEither()方法结合CompletableFuture的工具方法来实现超时处理。例如,我们可以使用CompletableFuture的anyOf()方法等待多个任务中任意一个完成,然后使用acceptEither()方法对完成的任务的结果进行消费操作。

三个重载版本声明如下:

public CompletableFuture<Void> acceptEither(CompletionStage<? extends T> other, Consumer<? super T> action) {
    return orAcceptStage(null, other, action);
}

public CompletableFuture<Void> acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action) {
    return orAcceptStage(asyncPool, other, action);
}

public CompletableFuture<Void> acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action,
                                                 Executor executor) {
    return orAcceptStage(screenExecutor(executor), other, action);
}
@Test
public void acceptEither() {
    CompletableFuture.supplyAsync(() -> {
        try {
            int timeout = ThreadLocalRandom.current().nextInt(5);
            System.out.println("A = " + timeout);
            TimeUnit.SECONDS.sleep(timeout);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("A");
        return "A";
    }).acceptEither(CompletableFuture.supplyAsync(() -> {
        try {
            int timeout = ThreadLocalRandom.current().nextInt(5);
            System.out.println("B = " + timeout);
            TimeUnit.SECONDS.sleep(timeout);
            System.out.println("B");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return "B";
    }), v -> System.out.println("acceptEither===" + v)).join();
}

总结

从上面的案例中可以看出:

  1. apply大都是Function类型的参数,接受一个或两个参数,返回一个值。
  2. accept大都是Consumer类型的参数,接受一个或两个参数,不返回值。
  3. run大都是Runnable类型的参数,不接收参数,也不返回值。

综合案例

泡茶喝实例

首先需要先完成分工方案,在下面的程序中,我们分3个任务:任务1负责洗水壶、烧开水,任务2负责洗茶壶、洗茶杯和拿茶叶,任务3负责泡茶。其中任务3要等待任务1和任务2都完成后才能开始。

@Test
public void drink() {
    // 任务一: 洗水壶, 烧开水
    CompletableFuture<Boolean> hotJob = CompletableFuture.supplyAsync(() -> {
        try {
            System.out.println("洗好水壶, 烧开水");
            TimeUnit.SECONDS.sleep(2);
            System.out.println("水开了");
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return true;
    });

    // 任务二: 洗茶壶、洗茶杯和拿茶叶
    CompletableFuture<Boolean> washJob = CompletableFuture.supplyAsync(() -> {
        try {
            System.out.println("洗茶杯");
            TimeUnit.SECONDS.sleep(2);
            System.out.println("洗完了");
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return true;
    });

    // 任务三: 负责泡茶
    String s = hotJob.thenCombine(washJob, (h, w) -> {
        if (h && w) {
            System.out.println("泡茶喝, 茶喝完");
            return "OK";
        }
        return "FF";
    }).join();
    System.out.println(s);

}

调用RPC实例

模拟调用rpc服务, 最终消耗的时间为单个服务的最长调用时间。

@Test
public void rpc() {
    String s = CompletableFuture.supplyAsync(() -> {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("rpc1");
        return "rpc1";
    }).thenCombine(CompletableFuture.supplyAsync(() -> {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("rpc2");
        return "rpc2";
    }), (l, r) -> l + "&" + r).join();
    System.out.println(s);
}