CompletableFuture 复杂问题与最佳实践(笔记)

709 阅读8分钟

前言

写文不易,禁止转载!

以笔记的形式总结了并发编程,特别是 CompletableFuture/ListenableFuture 常见的问题、实践与解决方法,涉及性能优化、线程池、并发思想等内容。由于很多问题过于深入,此笔记仅作抛砖引玉。建议感兴趣的读者通过谷歌、AI等方式学习,也欢迎与笔者进行交流。

避免上下文切换:Batch / 直接执行器

对于相似任务进行打包,避免上下文切换

直接执行器实现就地执行,非预期即为线程泄露。

提前数据加载。

应用反压

基于自动控制的前置限流(load regulation)。

使用无界资源是系统崩坏/稳定性问题的第一原因。

并发度控制。

接受的任务应该尽最大能力实现计算,不能接受的任务提前拒绝(熔断)。

可参考 Applying Back Pressure When Overloaded image.png

当系统在持续高负载下运行时,应该如何应对?如果系统继续接受请求直到响应时间急剧上升并最终崩溃,这显然不是理想的服务方式。更好的方法是以系统的最大吞吐量处理事务,同时保持良好的响应时间,并拒绝超过此到达率的请求。

通过一个小型艺术画廊的比喻来说明:画廊最多可容纳30名观众,如果超过这个人数,观众会感到不满。因此,建议多余的观众去附近的咖啡馆稍作等待,直到画廊不那么拥挤。类似地,在系统中,线程池的大小和处理单个事务的时间决定了可用容量。为了避免队列无限增长导致系统崩溃,应该限制输入队列的长度,从而应用“反压”机制。

反压通过限制队列长度来防止系统过载,并通过发送“服务器繁忙”消息(如HTTP 503状态码)来通知客户稍后再试。监控队列和线程的使用情况,并在达到阈值时采取适当措施,可以帮助系统在高负载下保持稳定。

总结来说,考虑系统在高负载下的行为是至关重要的,应用反压是一种有效的技术,可以在不降低已接受请求的性能的情况下应对持续的高负载。

超时处理

Futures.withTimeout 方法在处理异步操作时具有以下优点:

  1. 超时管理:它允许你为 ListenableFuture 指定一个最大完成时间。如果操作超过了指定的时间,它会通过抛出一个 TimeoutException(封装在 ExecutionException 中)来提前结束。这有助于防止操作无限期挂起,并确保你的应用程序在操作超出预期时间时能够恢复或采取替代措施。
  2. 资源效率:当超时时,withTimeout 会中断并取消委托的 future,从而释放那些被长时间运行或停滞的任务占用的资源。这在资源有限的环境中或处理大量并发任务时尤为有用。
  3. 提高可靠性:实现超时可以提高应用程序的可靠性,确保它能够处理外部服务或操作无响应的情况。这有助于维护系统的整体响应性和稳定性。
  4. 易于使用:该方法与 Guava 的 ListenableFuture 框架无缝集成,使得在现有异步代码中添加超时功能变得简单,无需进行大量重构。

取消传播

在异步编程中,任务取消的传播是一个重要的特性,它允许一个任务的取消状态影响其他相关任务。Guava 的 ListenableFuture 提供了一些机制来实现取消传播,这带来了多个好处:

  1. 资源管理:通过传播取消状态,可以确保相关任务不会继续消耗系统资源。这对于长时间运行的任务或需要大量计算资源的任务尤为重要。
  2. 简化逻辑:取消传播可以简化任务之间的依赖关系管理。当一个任务被取消时,所有依赖于该任务的后续任务也会被自动取消,从而避免了手动管理这些依赖关系的复杂性。
  3. 提高响应性:在某些情况下,用户可能希望快速终止操作(例如,用户取消了一个操作)。取消传播可以确保系统迅速响应用户的请求,而不是继续执行不必要的任务。
  4. 错误处理:当一个任务被取消时,相关任务也会被取消,这可以减少错误处理的复杂性,因为你不需要处理由于取消而导致的异常情况。

避免阻塞

  1. 添加回调,使用时带上ListenableFuture包装
  2. 任务分解,如 transformeAsync,避免阻塞任务嵌套
  3. 通过回调等待结果(回调主动唤醒),避免forEach等待,切换内核态

SingleFlight 模式

请求级别

SingleFlight 模式的核心思想和工作机制:

  1. 请求合并:当多个请求同时请求相同的资源时,SingleFlight 模式会将这些请求合并为一个单一的请求。只有第一个请求会触发实际的计算或数据获取操作,其余的请求会等待这个操作完成。
  2. 结果共享:一旦实际的操作完成,所有等待的请求都会收到相同的结果。这意味着只需要一次计算或数据获取,所有请求都能共享这个结果。
  3. 错误处理:如果实际的操作失败,所有等待的请求都会收到相同的错误信息,这样可以确保错误的一致性。

SingleFlight 模式的优点包括:

  • 减少重复计算:通过合并请求,可以显著减少系统的负载,特别是在高并发环境下。
  • 提高效率:避免了对相同数据的重复请求,提高了资源利用率。
  • 简化代码:通过集中处理请求逻辑,可以简化代码结构,减少错误处理的复杂性。

任务优先级

主要的优点:

  1. 任务调度灵活性:通过优先级机制,线程池可以根据任务的重要性或紧急程度来调度任务。这意味着关键任务可以被优先处理,而不必等待其他较低优先级任务完成。
  2. 资源优化:在资源有限的情况下,优先级任务调度可以确保关键任务获得足够的资源进行处理,从而优化资源的使用效率。这对于需要实时响应的系统尤为重要。
  3. 提高系统响应性:对于需要快速响应的应用程序,优先级任务调度可以确保高优先级任务迅速得到处理,从而提高系统的整体响应性和用户体验。
  4. 避免任务饥饿:通过合理设置任务优先级,可以避免某些任务长时间得不到处理(即任务饥饿问题),确保所有任务最终都能得到执行。
  5. 支持复杂业务逻辑:在复杂的业务场景中,不同任务可能有不同的优先级需求。支持优先级的线程池可以更好地满足这些业务需求,确保业务逻辑的正确性和效率。

ForkJoinPool

性能好,适用于递归 + 计算密集 + 大数据量任务。

任务不能阻塞!

严格性能测试。

快速失败(解包 - futures → List )

在处理多个异步任务时,通常我们希望在其中任何一个任务失败时立即终止所有任务的执行,并返回一个失败状态。

多个错误问题

多个任务抛出异常。

死锁/活锁

任务在任务队列中,而活跃线程阻塞。

  1. 新增线程池(父子线程池)
  2. 任务拆分,避免阻塞

并发安全

使用并发安全类

线程局限

happens-before

TTL 与上下文

注意其与线程有强绑定关系。

每个 CompletableFuture 隐形带一个线程(上下文),注意防止线程泄露。

为防止线程泄露,可以强制指定切换上下文。可参考CFFU中对于CF超时功能实现。

监控

多线程调用栈,IDEA 支持自定义父子调用栈。

www.jetbrains.com/help/idea/d…

定时采集线程池指标数据(20 多种指标,包含线程池维度、队列维度、任务维度、tps、tpxx 等),支持通过 MicroMeter、JsonLog、JMX 三种方式定时获取,也可以通过 SpringBoot Endpoint 端点实时获取最新指标数据,同时提供 SPI 接口可自定义扩展实现

包装类,Wrapper Chain。

虚拟线程

极大简化开发,同步形式开发

结构化并发

避免资源泄露,安全并发。同步形式实现,简单好上手。