Java中CompletableFuture中4种异步编程方法

Java的CompletableFuture框架提供了强大的异步编程能力,方便任务的并发执行。

1、runAsync() 与 SupplyAsync()之间的区别
CompletableFuture是 Java 中一个功能强大的框架,可以实现异步编程,方便同时执行任务而不会阻塞主线程。 runAsync()和SupplyAsync()是CompletableFuture类提供的方法。

在进行比较之前,让我们先了解runAsync()和SupplyAsync()的各个功能。这两种方法都会启动异步任务,使我们能够并发执行代码而不会阻塞主线程。

  • runAsync()是一种用于异步执行不产生结果的任务的方法。它适用于我们想要异步执行代码而不等待结果的“即发即忘”任务。例如,记录日志、发送通知或触发后台任务。
  • 另一方面,supplyAsync()是一种用于异步执行产生结果的任务的方法。它非常适合需要结果进行进一步处理的任务。例如,从数据库获取数据、进行 API 调用或异步执行计算。

runAsync()和SupplyAsync()之间的主要区别在于它们接受的输入和它们生成的返回值的类型。

runAsync()
当要执行的异步任务没有产生任何结果时,使用runAsync ()方法。它接受Runnable功能接口并异步启动任务。它返回CompletableFuture<Void>,对于重点是完成任务而不是获取特定结果的场景非常有用。

这是展示其用法的片段:

CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    // Perform non-result producing task
    System.out.println(
"Task executed asynchronously");
});

在此示例中,runAsync()方法用于异步执行不产生结果的任务。提供的 lambda 表达式封装了要执行的任务。完成后,打印:
Task completed successfully

SupplyAsync()
另一方面,当异步任务产生结果时,将使用SupplyAsync() 。它接受供应商功能接口并异步启动任务。随后,它返回一个CompletableFuture<T>,其中T是任务生成的结果的类型。

让我们用一个例子来说明这一点:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    // Perform result-producing task
    return
"Result of the asynchronous computation";
});
//稍后获取结果
String result = future.get();
System.out.println(
"Result: " + result);

在此示例中,supplyAsync()用于异步执行结果生成任务。SupplyAsync()中的 lambda 表达式表示异步计算结果的任务。完成后,打印得到的结果:
Result: Result of the asynchronous computation

两者运行区别:

  • 使用runAsync()时,任务会立即在公共线程池中启动。它的行为反映了调用新的Thread(runnable).start()。这意味着任务在调用后立即开始执行,没有任何延迟或调度考虑。
  • supplyAsync()在公共线程池中调度任务,如果其他任务排队,则可能会延迟其执行。这种调度方法有利于资源管理,因为它有助于防止线程创建的突然爆发。通过对任务进行排队并根据线程的可用性调度其执行,supplyAsync()可确保高效的资源利用。

2、thenApply() 和 thenApplyAsync() 之间的区别
在CompletableFuture框架中,thenApply()和thenApplyAsync()是促进异步编程的关键方法。

CompletableFuture提供了thenApply()和thenApplyAsync()方法,用于将转换应用于计算结果。这两种方法都可以对CompletableFuture的结果执行链接操作。

  • thenApply()是一种用于在CompletableFuture完成时将函数应用于其结果的方法。它接受Function函数式接口,将该函数应用于结果,并返回一个带有转换结果的新CompletableFuture 。
  • thenApplyAsync()是异步执行所提供函数的方法。它接受一个Function功能接口和一个可选的Executor,并返回一个新的CompletableFuture和异步转换的结果。

thenApply()和thenApplyAsync()之间的主要区别在于它们的执行行为。

thenApply()
默认情况下,thenApply()方法使用完成当前CompletableFuture 的同一线程执行转换函数。这意味着转换函数的执行可以在结果可用后立即发生。如果转换函数长时间运行或占用资源,这可能会阻塞线程。

但是,如果我们在尚未完成的CompletableFuture上调用thenApply() ,它将在执行程序池的另一个线程中异步执行转换函数。

这是说明thenApply() 的代码片段:

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 5);
CompletableFuture<String> thenApplyResultFuture = future.thenApply(num -> "Result: " + num);
String thenApplyResult = thenApplyResultFuture.join();
assertEquals(
"Result: 5", thenApplyResult);

在此示例中,如果结果已经可用并且当前线程兼容,则thenApply()可能会同步执行该函数。但是,需要注意的是,CompletableFuture会根据各种因素(例如结果的可用性和线程上下文)智能地决定是同步执行还是异步执行。


thenApply ()方法在以下场景中特别有用:

  • 顺序转换:当需要对CompletableFuture的结果顺序应用转换时。这可能涉及将数字结果转换为字符串或根据结果执行计算等任务。
  • 轻量级操作:它非常适合执行小型、快速的转换,不会对调用线程造成明显的阻塞。示例包括将数字转换为字符串、根据结果执行计算或操作数据结构。


thenApplyAsync()
相比之下,thenApplyAsync()通过利用执行器池(通常是ForkJoinPool.commonPool() )中的线程来保证所提供函数的异步执行。这确保了该函数是异步执行的,并且可以在单独的线程中运行,从而防止当前线程的任何阻塞。

以下是我们如何使用thenApplyAsync():

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 5);
CompletableFuture<String> thenApplyAsyncResultFuture = future.thenApplyAsync(num -> "Result: " + num);
String thenApplyAsyncResult = thenApplyAsyncResultFuture.join();
assertEquals(
"Result: 5", thenApplyAsyncResult);

在此示例中,即使结果立即可用,  thenApplyAsync() 也始终安排该函数在单独的线程上异步执行。

thenApplyAsync()方法适用于以下情况:

  • 异步转换:当需要异步应用转换时,可能会利用多个线程并行执行。例如,在用户上传图像进行编辑的 Web 应用程序中,使用CompletableFuture进行异步转换有利于同时应用调整大小、滤镜和水印,从而提高处理效率和用户体验。
  • 阻塞操作:如果转换函数涉及阻塞操作、I/O 操作或计算密集型任务,则thenApplyAsync()会变得有利。通过将此类计算卸载到单独的线程,有助于防止阻塞调用线程,从而确保更流畅的应用程序性能。

以上方法链式操作
runAsync()方法
runAsync()
方法不能直接与thenApply()或thenAccept()等方法链接,因为它不产生结果。但是,我们可以在runAsync()任务完成后使用thenRun()执行另一个任务。此方法允许我们链接附加任务以顺序执行,而不依赖于初始任务的结果。

下面的示例展示了使用runAsync()和thenRun()进行链接操作:

CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    System.out.println("Task executed asynchronously");
});
future.thenRun(() -> {
   
// 在 runAsync() 完成后执行另一个任务
    System.out.println(
"Another task executed after runAsync() completes");
});

在此示例中,我们首先使用runAsync()异步执行任务。然后,我们使用thenRun()指定在初始任务完成后要执行的另一个任务。这允许我们按顺序链接多个任务,从而产生以下输出:

Task executed asynchronously
Another task executed after runAsync() completes

supplyAsync()方法
相反,supplyAsync()由于其返回值而允许链接操作。由于SupplyAsync()会生成结果,因此我们可以使用thenApply()等方法来转换结果,使用thenAccept()来使用结果,或者使用thenCompose()来链接进一步的异步操作。这种灵活性使我们能够通过将多个任务链接在一起来构建复杂的异步工作流程。

下面的示例说明了SupplyAsync()和thenApply()的链接操作:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    return "Result of the asynchronous computation";
});
future.thenApply(result -> {
   
// Transform the result
    return result.toUpperCase();
}).thenAccept(transformedResult -> {
   
// Consume the transformed result
    System.out.println(
"Transformed Result: " + transformedResult);
});

在此示例中,我们首先使用SupplyAsync()异步执行任务,这会生成结果。然后,我们使用thenApply()来转换结果,然后使用thenAccept()来使用转换后的结果。这演示了使用SupplyAsync()链接多个操作,从而允许更复杂的异步工作流程。

下面是输出的示例:

Transformed Result: RESULT OF THE ASYNCHRONOUS COMPUTATION