实战 CompletableFuture:从图片收集节点看异步并发编程

实战 CompletableFuture:从图片收集节点看异步并发编程

在开发智能图片收集工作流时,我们遇到了一个典型场景:根据用户输入的提示词,系统需要同时进行多种图片资源的检索与生成------搜索内容图片、查找插画、生成 Mermaid 架构图、设计 Logo。如果这些任务串行执行,响应时间将随着任务数量线性增长,难以接受。

我们使用 Java 的 CompletableFuture 实现了任务的并发执行,将总耗时压缩到接近最慢单任务的耗时。本文会结合真实的 ImageCollectorNode 代码,逐行解析 CompletableFuture 在实战中的落地方式,让你彻底理解如何利用它构建高效的异步流水线。


1. 场景回顾:我们需要做什么?

ImageCollectorNode 是 LangGraph4j 工作流中的一个节点,职责是收集所有需要的图片。它的核心逻辑是:

  1. 通过 ImageCollectionPlanService 生成一个"图片收集计划"(ImageCollectionPlan),该计划包含四类任务列表:

    • 内容图片搜索任务(contentImageTasks
    • 插画搜索任务(illustrationTasks
    • 架构图生成任务(diagramTasks
    • Logo 生成任务(logoTasks
  2. 并发执行 所有这些子任务,每个子任务返回一组 ImageResource 对象。

  3. 等待所有子任务完成,合并结果,存入工作流上下文。


2. 为什么选择 CompletableFuture?

在 Java 中,实现并发的方式有很多:Thread + RunnableExecutorService + FutureCompletableFuture 等。我们选择 CompletableFuture 的原因:

  • 声明式异步 :通过 supplyAsync 一行代码即可提交任务,无需手动管理线程。
  • 丰富的组合操作allOf 可以优雅地等待多个任务完成,比 ExecutorService.invokeAll() 更灵活。
  • 非阻塞回调(虽然本例未使用,但为扩展预留)。
  • 异常处理链 :可在后续添加 exceptionally 进行降级。

更重要的是,CompletableFuture 与函数式编程风格契合,代码可读性高,适合这种"提交-收集"模式。


3. 代码逐段解析

3.1 存储所有异步任务的句柄

java 复制代码
List<CompletableFuture<List<ImageResource>>> futures = new ArrayList<>();
  • 这个列表存放的是每一个子任务的异步计算结果引用
  • 泛型 CompletableFuture<List<ImageResource>> 表示每个任务执行完毕后会返回一个 List<ImageResource>
  • 我们并不在创建时获取结果,而是先把所有任务的"凭证"保存下来,便于后续统一等待和收集。

3.2 提交异步任务:CompletableFuture.supplyAsync

以内容图片搜索为例:

java 复制代码
if (plan.getContentImageTasks() != null) {
    ImageSearchTool imageSearchTool = SpringContextUtil.getBean(ImageSearchTool.class);
    for (ImageCollectionPlan.ImageSearchTask task : plan.getContentImageTasks()) {
        futures.add(CompletableFuture.supplyAsync(() ->
                imageSearchTool.searchContentImages(task.query())));
    }
}
  • CompletableFuture.supplyAsync(Supplier<U> supplier):提交一个有返回值 的异步任务,返回 CompletableFuture<U>
    • 这里的 Supplier() -> imageSearchTool.searchContentImages(task.query()),它不接受参数,调用搜索方法并返回图片列表。
    • 任务会被提交到默认的 ForkJoinPool.commonPool() 中执行(实际生产环境建议自定义线程池)。
  • futures.add(...) 将返回的 CompletableFuture 加入到列表中,此时任务已经开始在后台执行,主线程不会阻塞。

类似地,插画搜索、Mermaid 图生成、Logo 生成也采用完全一样的模式,只是调用的工具类和方法不同。注意每个任务都独立包装为一个 CompletableFuture,所有任务并行执行


3.3 等待所有任务完成:CompletableFuture.allOf(...).join()

java 复制代码
CompletableFuture<Void> allTasks = CompletableFuture.allOf(
        futures.toArray(new CompletableFuture[0]));
allTasks.join();
  • CompletableFuture.allOf(CompletableFuture<?>... cfs):接收一个 CompletableFuture 数组,返回一个新的 CompletableFuture<Void>,当所有 传入的 CompletableFuture 都完成时,这个新 CompletableFuture 才会完成。
  • futures.toArray(new CompletableFuture[0]) 将列表转为数组,因为 allOf 接受可变参数。
  • join() 方法会阻塞当前线程,直到 allTasks 完成(即所有子任务都完成)。与 get() 类似,但 join() 不抛出受检异常,更适合在非异常流中使用。

此时,所有子任务都已执行完毕(无论是正常完成还是异常完成),我们可以安全地收集结果。


3.4 收集结果:future.get()

java 复制代码
for (CompletableFuture<List<ImageResource>> future : futures) {
    List<ImageResource> images = future.get();
    if (images != null) {
        collectedImages.addAll(images);
    }
}
  • 遍历 futures 列表,对每个 CompletableFuture 调用 get() 方法获取结果。
  • 由于我们已经用 join() 等待了所有任务完成,此时 get() 会立即返回(不会阻塞),或者如果任务异常完成,get() 会抛出 ExecutionException(但我们用 try-catch 包裹了整个流程,所以不会中断)。
  • 将每个任务返回的图片列表合并到总的 collectedImages 中。

3.5 异常处理与容错

java 复制代码
try {
    // ... 所有并发逻辑
} catch (Exception e) {
    log.error("图片收集失败: {}", e.getMessage(), e);
}
// 即使发生异常,也会继续执行,并保存已收集到的图片
  • 我们将整个收集过程包裹在 try-catch 中,确保即使某个子任务抛出异常,也不会导致整个节点崩溃。
  • 如果某个 future.get() 抛出异常,会被捕获,但已成功完成的其他任务的结果仍然会被保留在 collectedImages 中。
  • 这种"部分成功"的容错策略在分布式系统中非常实用。

4. 进阶优化建议

4.1 自定义线程池

默认的 ForkJoinPool.commonPool() 是全局共享的,可能会被其他业务干扰,且默认并行度较低。建议创建专用的线程池:

java 复制代码
Executor executor = Executors.newFixedThreadPool(10);
CompletableFuture.supplyAsync(() -> ..., executor);

executor 作为第二个参数传递给 supplyAsync,确保图片收集任务使用独立的线程资源。

4.2 超时控制

如果某些任务响应很慢,可能导致整体等待时间过长。可以使用 Java 9+ 的 orTimeout 方法:

java 复制代码
CompletableFuture.supplyAsync(() -> ...)
    .orTimeout(5, TimeUnit.SECONDS)
    .exceptionally(ex -> Collections.emptyList()); // 超时返回空列表

这样单个任务最多等待 5 秒,避免拖垮整体。

4.3 异常精细化处理

future.get() 时,如果某个任务异常,可以单独捕获并记录,而不会影响其他任务的结果收集。更优雅的方式是在每个 CompletableFuture 上添加 exceptionally 降级:

java 复制代码
CompletableFuture<List<ImageResource>> safeFuture = 
    CompletableFuture.supplyAsync(() -> ...)
        .exceptionally(ex -> {
            log.warn("任务失败,返回空列表", ex);
            return Collections.emptyList();
        });

这样 get() 永远返回非 null 的列表,简化合并逻辑。


5. 总结

通过 CompletableFuture,我们仅用十余行核心代码就实现了多类型、多数量任务的并发收集,大幅提升了系统吞吐量。回顾一下我们使用的关键 API:

方法 作用
supplyAsync 提交有返回值的异步任务,返回 CompletableFuture
allOf 组合多个 CompletableFuture,等待全部完成
join() 阻塞等待完成,与 get() 类似但无需处理受检异常
get() 获取异步任务的结果(在任务已完成时立即返回)

在实际项目中,CompletableFuture 还能提供更强大的编排能力,例如 thenApply 链式转换、thenCombine 合并两个任务、handle 统一处理结果和异常等。但"提交-等待-收集"模式已经能满足绝大多数批量并发的需求。

相关推荐
hey_sml41 分钟前
JAVA每日一学---CompletableFuture中thenApply与thenCompose区别详解
java·开发语言
Doraemomo44 分钟前
数据结构-环形链表
java·数据结构·链表
飞哥数智坊1 小时前
实测7套 Code Agent组合:最终效果,真不只取决于模型
ai编程
东小西2 小时前
番外篇二:《不到十行代码,我用ReactAgen搭了个会自己调工具的 Agent》
openai·ai编程
萧瑟余晖2 小时前
Java深入解析篇十七之SpringCloud
java·开发语言
是未才3 小时前
从输入 URL 到页面返回:DNS、路由、TLS 与 HTTP 完整链路
java·后端·计算机网络
ttod_qzstudio3 小时前
Java 常用语法极简通关(五):类与对象——字段、方法、构造器、this 与 static
java·开发语言·python
萧瑟余晖4 小时前
Java深入解析篇十七之Spring Security
java·开发语言·spring
kyriewen5 小时前
我用Claude Code两天干完了团队两周的排期——周报发出去那一刻我就后悔了
前端·javascript·ai编程
不老刘5 小时前
【Java入门】Java服务部署方式全景对比:从java -jar到K8s
java·kubernetes·jar