Bootstrap

【异步编程】CompletableFuture:异步任务的选择(执行最快的)执行

对于两个异步任务,我们有时希望在其中一个任务完成时立即执行某些操作,而不必等待其他任务的完成。为了实现这一需求,CompletionStage 提供了几个有用的方法,其中包括 applyToEither()runAfterEither()acceptEither()

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

一. applyToEither : 拿到第一个任务结束的结果

applyToEither() 方法允许我们在两个异步任务中选择一个已经完成的任务,并对其结果应用一个函数。无论哪个任务先完成,都会执行传入的 Function 操作,并返回一个新的 CompletableFuture

方法签名:

public <U> CompletableFuture<U> applyToEither(CompletionStage<? extends T> other, Function<? super T, ? extends U> fn)

- **`other`**:另一个 `CompletionStage`,与当前任务并行执行。
- **`fn`**(两个任务中)当第一个任务完成时应用的函数。

使用场景:

applyToEither() 方法非常适合于需要在多个异步任务中选择一个最快完成的任务结果并进行处理的场景。

示例代码:

@Test  
public void applyToEitherDemo2() throws Exception {  
  
    CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {  
        try {  
            Thread.sleep(500);  
        } catch (InterruptedException e) {  
            Thread.currentThread().interrupt();  
        }  
        return 2;  
    });  
  
    CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {  
        try {  
            Thread.sleep(100);  
        } catch (InterruptedException e) {  
            Thread.currentThread().interrupt();  
        }  
        return 3;  
    });  
  
    CompletableFuture<Integer> result = future1.applyToEither(future2, (result1) -> {  
        System.out.println("第一个完成的任务的结果: " + result1);  
        return result1 * 2;  // 对结果进行处理  
    });  
    Integer integer = result.get();  
    System.out.println(integer);  
  
}


分析

  • future1future2 两个异步任务并行执行。
  • applyToEither() 会选择第一个完成的任务,并将其结果传递给 fn
    这里是对结果进行乘以 2 的操作。
  • 如果 future2 完成更快(如上例中的500ms),则会输出 "第一个完成的任务的结果: 3" 并返回 6

 

二. runAfterEither :第一个任务完成后执行副作用

runAfterEither() 方法与 applyToEither() 类似,但它不关心任务的结果。它只在其中一个任务完成时执行一个没有返回值的操作。它适用于需要在多个任务中选择一个最先完成的任务并执行某个副作用操作(如打印日志、更新状态等)的场景。

方法签名:

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

- **`other`**:另一个 `CompletionStage`。
- **`action`**:当第一个任务完成时执行的 `Runnable` 操作。
- **返回类型**:返回一个新的 `CompletableFuture<Void>`,表示操作完成。

使用场景:

runAfterEither() 方法适用于当你只关心某个任务完成后执行某些副作用操作,而不需要处理任务的结果时。

示例代码:

@Test  
public void test() {  
    CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {  
        try {  
            Thread.sleep(1000);  
        } catch (InterruptedException e) {  
            Thread.currentThread().interrupt();  
        }  
        return 2;  
    });  
  
    CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {  
        try {  
            Thread.sleep(500);  
        } catch (InterruptedException e) {  
            Thread.currentThread().interrupt();  
        }  
        return 3;  
    });  
  
    future1.runAfterEither(future2, () -> System.out.println("第一个完成的任务已经结束"));  
}

分析

  • future1future2 分别执行两个异步任务。
  • runAfterEither() 会在第一个任务完成后执行 Runnable 操作,这里是打印 "第一个完成的任务已经结束"
  • 无论哪个任务先完成,都会触发打印操作,而不关心它们的计算结果。

 

三. acceptEither:消费第一个任务的结果

acceptEither() 方法与 applyToEither() 方法类似,不同之处在于它接受一个 BiConsumer 来消费完成任务的结果,而不是返回一个新的 CompletableFuture。该方法适用于只需要消费结果的场景。

方法签名:

public void acceptEither(CompletionStage<? extends T> other, 
						 Consumer<? super T> action)



- **`other`**:另一个 `CompletionStage`。
- **`action`**:当第一个任务完成时执行的 `Consumer` 操作。
- **返回类型**:该方法没有返回值,它会直接消费任务的结果。

使用场景:
acceptEither() 方法适用于当我们只关心任务的结果并需要消费(如打印、记录日志等)时,但不需要返回任何新的 CompletableFuture

示例代码:

@Test  
public void test2() {  
    CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {  
        try {  
            Thread.sleep(1000);  
        } catch (InterruptedException e) {  
            Thread.currentThread().interrupt();  
        }  
        return 2;  
    });  
  
    CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {  
        try {  
            Thread.sleep(500);  
        } catch (InterruptedException e) {  
            Thread.currentThread().interrupt();  
        }  
        return 3;  
    });  
  
    //使用 join() 阻塞直到操作完成  
    future1.acceptEither(future2, (result) -> System.out.println("第一个完成的任务的结果: " + result)).join();  
  
}

**分析**- `future1` 和 `future2` 分别执行异步任务。
- `acceptEither()` 会选择第一个完成的任务,并将其结果传递给 `Consumer` 进行处理。在这里,我们将结果打印出来。
- 输出会是 `"第一个完成的任务的结果: 3"`,因为 `future2` 在500ms时先完成。

在测试代码中,通常会使用 join() 来确保异步任务完成后,主线程再退出,避免出现异步回调没有被执行的情况。

四. 三种接口总结

  • applyToEither():在两个异步任务中选择一个最先完成的任务,并对其结果应用一个函数,返回一个新的 CompletableFuture
  • runAfterEither():在两个任务中选择一个最先完成的任务,不关心任务的结果,只执行一个 Runnable 操作。
  • acceptEither():在两个任务中选择一个最先完成的任务,并对其结果执行一个 Consumer 操作,适用于消费任务的结果。

这三个方法提供了灵活的工具,可以帮助我们在多个并行任务中选择一个最先完成的任务,并根据需求对其结果进行处理或执行副作用操作。掌握这些方法,可以让我们更高效地进行异步编程和任务调度。

;