Java中CompletableFuture的thenApply与thenApplyAsync比较

Java 的 CompletableFuture 类提供了两种关键方法,thenApply 和 thenApplyAsync,用于处理异步计算的结果。虽然这两种方法的用途相同,但它们的细微差别可能会显著影响程序的性能和并发性。

本文探讨了 thenApply 和 thenApplyAsync 之间的区别,并根据应用程序的具体要求,提供何时使用每种方法的见解。

`thenApply` 和 `thenApplyAsync` 都是 Java 的 CompletableFuture 类中的方法,它们用于在计算结果可用时对其进行处理。然而,它们之间存在一些关键差异,这些差异可能会影响程序的性能和行为。

1.执行线程:
`thenApply` 和 `thenApplyAsync` 之间的主要区别在于它们执行的线程。

  • `thenApply` 在完成前一个 CompletableFuture 的同一线程上执行回调函数,
  • 而 `thenApplyAsync` 在从 ForkJoinPool.commonPool()(默认)或给定的 Executor 获取的不同线程上执行回调函数。

2.非阻塞:
`thenApplyAsync` 是非阻塞的。它可以将后续完成阶段卸载到其他线程,同时允许当前线程执行其他工作。这对于希望充分利用系统资源的高并发程序非常有用。

3.性能:

  • 当回调函数是长时间运行的任务时,涉及阻塞操作、I/O 操作或计算密集型任务时,`thenApplyAsync` 会更高效,因为它不会阻塞主线程。
  • 但是,如果任务很短且很快,`thenApply` 可能更可取,因为它避免了创建新任务并将其安排在不同线程上运行的开销。

场景案例
让我们考虑一个现实世界的场景:一个在线购物系统。
在这个系统中,当用户下订单时,会发生几个步骤:

  1. 订单详细信息保存在数据库中。
  2. 向用户发送电子邮件确认。
  3. 订单被送往仓库进行包装和发货。

每个步骤都可以用 CompletableFuture 来表示,并且可以使用thenApply或thenApplyAsync将它们链接在一起。

使用方法如下thenApply:

CompletableFuture.supplyAsync(() -> saveOrderToDatabase(order))
    .thenApply(orderId -> sendConfirmationEmail(orderId))
    .thenApply(emailSuccess -> sendOrderToWarehouse(order));


在这种情况下,每个步骤都将在同一线程上执行。
如果发送确认电子邮件(sendConfirmationEmail(orderId))需要很长时间,就会阻塞线程直到发送完成,从而延迟发送订单到仓库(sendOrderToWarehouse(order))操作。

使用thenApplyAsync:

CompletableFuture.supplyAsync(() -> saveOrderToDatabase(order))
    .thenApplyAsync(orderId -> sendConfirmationEmail(orderId))
    .thenApplyAsync(emailSuccess -> sendOrderToWarehouse(order));


在这种情况下,每个步骤都将在不同的线程中执行。如果发送确认电子邮件需要很长时间,则不会阻塞将订单保存到数据库的线程。相反,它会被卸载到一个单独的线程中,一旦订单被保存到数据库,就可以立即开始 sendOrderToWarehouse(order) 操作。

这是一个简化的示例,但它说明了 thenApplyAsync 的优势,在这种情况下,操作可能会长期运行,而你又想避免阻塞线程。

当调用 thenApply 和 thenApplyAsync 时,它们所链到的 CompletableFuture 是否完成?这就是两者区别所在,详解如下:

  • 在 thenApply 的情况下,如果 CompletableFuture 已经完成,那么如果调用线程尚未开始处理结果(即如果结果处理尚未开始),它将立即在调用线程上运行回调。如果回调是一个长期运行的操作,这可能会阻塞调用线程。
  • 无论 CompletableFuture 是否已经完成,thenApplyAsync 始终会在 ForkJoinPool(或提供的 Executor)之外的单独线程中运行回调。这样,即使回调是一个长期运行的操作,也能确保调用线程不会被阻塞。

总之:
因此,如果您有一个回调,该回调可能是长时间运行的操作,并且您不想冒险阻塞添加回调的线程,那么这thenApplyAsync将是有益的。它提供了更一致的异步行为,确保操作始终被转移到不同的线程。

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

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

另一方面,thenApplyAsync()方法适用于以下情况:

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

两者主要区别是背后线程机制:

  1. thenApply ():执行行为与前一阶段相同的线程或来自执行器池的单独线程(如果在完成之前调用)将线程与执行器池分开
  2. thenApplyAsync:将线程与执行器池分开,会在默认 ForkJoinPool.commonPool()中重新开启新线程,再次利用 ForkJoin并发高性能。

在本文中,我们探讨了CompletableFuture框架中thenApply()和thenApplyAsync()方法之间的功能和差异。
thenApply()可能会阻塞线程,因此它适合轻量级转换或可以接受同步执行的场景。另一方面, thenApplyAsync()保证异步执行,因此它非常适合涉及潜在阻塞的操作或计算密集型任务,这些任务对响应性至关重要。