Java StructuredTaskScope 怎样收集部分成功结果
当一组并发子任务中有几项成功、几项失败时,StructuredTaskScope.open() 的默认策略并不会自动给你一份“成功项列表”:默认策略面向的是全部成功或任一失败。要收集部分成功结果,应为作用域提供自定义 StructuredTaskScope.Joiner,在 onComplete 中只接收 SUCCESS 状态,再让 join() 返回收集结果。
官方地址:https://openjdk.org/jeps/505
本文按当前 Java SE 27 API 的预览写法说明。StructuredTaskScope 和 Joiner 属于预览 API,编译与运行时需要启用预览开关;不同 JDK 版本的 Joiner 泛型和异常签名可能不同,升级时要以对应版本的官方 API 为准。

先判断你要的是哪一种结果
StructuredTaskScope 把多个子任务放进同一个作用域,调用 fork 启动任务,再由作用域拥有者调用一次 join 等待结果。真正决定“失败后返回什么”的不是 fork,而是 Joiner 的策略。
| 策略 | join 返回 | 适合场景 |
|---|---|---|
allSuccessfulOrThrow() | 全部成功结果;任一失败就抛异常 | 聚合接口必须完整 |
anySuccessfulOrThrow() | 任一成功结果;全部失败才抛异常 | 多来源竞速或备选服务 |
自定义 Joiner | 按业务规则返回部分结果 | 允许降级、保留成功项 |
因此,标题中的“部分成功”不是把 allSuccessfulOrThrow 的异常吞掉,而是把完成事件交给一个明确的收集策略。失败项是否记录、是否提前取消、超时后是否返回已有结果,都应在这个策略里写清楚。
先用最小自定义 Joiner 收集成功值
下面的示例以 Java 27 的三泛型 Joiner API 为例:第一个泛型是子任务结果类型,第二个是 join() 的返回类型,第三个是 Joiner 可能抛出的异常类型。onComplete 可能被多个线程并发调用,所以不能用没有同步保护的普通 ArrayList。
import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.StructuredTaskScope; final class PartialSuccessJoinerimplements StructuredTaskScope.Joiner , RuntimeException> { // 完成回调可能并发执行,用线程安全队列保存成功值。 private final ConcurrentLinkedQueue successes = new ConcurrentLinkedQueue(); @Override public boolean onComplete(StructuredTaskScope.Subtask extends T> subtask) { // 只有 SUCCESS 才能调用 get,FAILED 和 UNAVAILABLE 没有可读结果。 if (subtask.state() == StructuredTaskScope.Subtask.State.SUCCESS) { successes.add(subtask.get()); } // false 表示不要因为一个完成事件就取消剩余子任务。 return false; } @Override public List result() { // join 等待作用域内任务完成后,把当前成功值冻结成不可变列表。 return List.copyOf(successes); } @Override public List timeout() { // 这里选择超时也返回已经收集的成功值;严格业务可改为抛出异常。 return List.copyOf(successes); } } public class PartialSuccessDemo { static List collectPartial(List > tasks) throws InterruptedException { try (var scope = StructuredTaskScope.open(new PartialSuccessJoiner ())) { // fork 只负责启动,结果由 Joiner 的完成回调统一收集。 tasks.forEach(scope::fork); return scope.join(); } // try-with-resources 负责关闭作用域并等待子线程结束。 } }
这段实现的关键不是“忽略异常”,而是只在状态为 SUCCESS 时调用 get()。失败子任务仍然会完成并触发回调,只是没有被放进成功值队列;作用域不会因为单个失败而提前取消。
为什么必须使用线程安全收集器
Joiner 的 onComplete 回调由子任务完成事件触发,多个子任务可能在相近时刻完成。一个普通的 ArrayList 在并发追加时会有数据竞争,轻则丢项,重则破坏内部数组状态。
ConcurrentLinkedQueue 适合这个收集点:回调只追加,最终在 result() 中通过 List.copyOf 生成稳定快照。快照的顺序是完成顺序,不是 fork 的提交顺序;如果业务要求按输入顺序返回,应把任务编号和值一起收集,再在返回前排序。
还有一个重要边界:Subtask.get() 只应在子任务状态为 SUCCESS 时调用。FAILED 表示任务抛出了异常,UNAVAILABLE 表示结果不可用,例如作用域被取消后任务尚未完成。把所有状态都直接 get() 会把“部分成功”再次变成异常路径。
三种策略放在一起就能看出差异

allSuccessfulOrThrow 适合“少一个结果就不能继续”的聚合任务;它的返回值表达完整成功。anySuccessfulOrThrow 适合“拿到一个可用结果就够了”的竞速任务,成功后可能取消还在执行的任务。自定义 Joiner 则把取消条件和返回值拆开,可以让失败项不影响已经成功的结果。
不要为了得到部分结果而捕获 ExecutionException 后再尝试读取所有子任务。默认 Joiner 的失败语义已经决定了作用域的结束方式,事后捕获无法可靠地表达“哪些任务已经成功、哪些任务因为取消而不可用”。在事件回调里记录状态,才有清晰的边界。
用预期状态判断实现是否符合需求
假设有三个任务:A 返回“库存”、B 返回“价格”、C 抛出异常。自定义 Joiner 的结果应当是包含 A、B 的列表,而不是因为 C 失败就整体抛错。若 C 先失败,也不能因此读取 C 的值;它只会被忽略或写入单独的失败记录。
List> tasks = List.of( () -> "库存: 有货", () -> "价格: 99", () -> { throw new IllegalStateException("推荐服务暂不可用"); } ); // 这里的调用只消费成功结果;InterruptedException 仍需交给上层处理。 List available = PartialSuccessDemo.collectPartial(tasks); // 结果的元素顺序按完成先后变化,不应把它当成输入顺序。 System.out.println(available);
如果业务还要展示失败原因,可以在 Joiner 内增加一个线程安全的失败集合,例如保存任务标识与异常对象;不要把异常文本拼进成功值列表。这样上层可以分别决定“展示已有数据”“记录降级原因”以及“是否触发补偿”。
超时、取消和预览 API 要单独处理
示例中的 timeout() 选择返回截至超时已经收集的成功值,但这是一项业务决定,不是所有场景都安全。对于结算、扣款、写入这类必须完整的操作,超时应抛出异常并交给上层回滚或重试;对于搜索聚合、推荐、可选指标,返回部分结果才可能合理。
同时要注意预览 API 的版本边界:
- 需要在编译和运行时启用
--enable-preview,并配合目标 JDK 的--release。 - JDK 25、26、27 的 StructuredTaskScope 与 Joiner 仍在演进,Joiner 的泛型、异常类型和方法名可能不同。
- 作用域只能由拥有者线程执行
fork、join和关闭;不要把 scope 交给普通工作线程操作。 - 关闭作用域仍会等待其子线程结束;部分成功不等于可以遗留未收尾的线程。
# 使用与目标 JDK 对应的预览开关编译和运行示例。 javac --enable-preview --release 27 PartialSuccessDemo.java java --enable-preview PartialSuccessDemo
最后按这张表选择实现
| 需求 | 推荐策略 | 关键风险 |
|---|---|---|
| 所有依赖都必须成功 | allSuccessfulOrThrow | 任一失败都会让整体失败 |
| 只要一个可用结果 | anySuccessfulOrThrow | 其它任务可能被取消 |
| 成功项可用,失败项降级 | 自定义 Joiner | 必须设计状态收集、超时和错误记录 |
所以,Java StructuredTaskScope 收集部分成功结果的落点是自定义 Joiner:在 onComplete 中检查状态、用线程安全容器收集成功值,再由 result() 或 timeout() 决定返回边界。只要把失败、取消、顺序和预览版本写进契约,部分成功就不会变成含糊的异常吞噬。
相关问题
失败子任务能不能直接调用 get?
不能。先判断状态为 SUCCESS,只有成功状态才有可读结果。
成功结果的顺序固定吗?
自定义队列通常反映完成顺序,不保证输入顺序。需要稳定顺序时,请同时保存任务编号并在返回前排序。
为什么不用 anySuccessfulOrThrow?
它表达的是“任一成功即可”,成功后会取消其它任务;如果需要保留多个成功值,就应使用自定义 Joiner。
Java ModuleDescriptor.Provides 怎样读取服务提供者的类名
- 上一篇
- Java ModuleDescriptor.Provides 怎样读取服务提供者的类名
- 下一篇
- Java virtual threads 怎样让任务支持可取消超时
-
- 文章 · java教程 | 24分钟前 | Java · 语言特性 · java super Java 25 Flexible Constructor Bodies
- Java 25 flexible constructor bodies 怎样在 super 前准备参数
- 212浏览 收藏
-
- 文章 · java教程 | 25分钟前 | Java 25 · java Java 25 Compact Source Files instance main
- Java 25 简洁源文件与实例 main 的启动入口配置方法
- 106浏览 收藏
-
- 文章 · java教程 | 1天前 |
- Java String.indent 为什么会自动补换行符
- 173浏览 收藏
-
- 文章 · java教程 | 1天前 |
- Java CompactNumberFormat 怎样按 Locale 格式化大数字
- 266浏览 收藏
-
- 文章 · java教程 | 1天前 |
- Java String.lines 怎样统一处理 CRLF 和 LF
- 372浏览 收藏
-
- 文章 · java教程 | 1天前 |
- Java MappedByteBuffer load 和 isLoaded 有什么区别
- 243浏览 收藏
-
- 文章 · java教程 | 1天前 |
- Java MappedByteBuffer.force 怎样只刷指定文件范围
- 330浏览 收藏
-
- 文章 · java教程 | 1天前 | Java · nio · java Slice ByteBuffer duplicate
- Java ByteBuffer duplicate 和 slice 修改数据有什么区别
- 498浏览 收藏
-
- 文章 · java教程 | 1天前 |
- Java ByteBuffer.alignedSlice 怎样按内存对齐创建切片
- 322浏览 收藏
-
- 文章 · java教程 | 1天前 |
- Java UserPrincipalLookupService 查找用户组失败怎么办
- 462浏览 收藏
-
- 文章 · java教程 | 1天前 |
- Java AclFileAttributeView 怎样读取文件的 ACL 条目
- 110浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 912次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 919次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 1003次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 995次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 815次使用
-
- Go map 并发写 panic 怎么办:从共享 map 到可控写入路径
- 2026-06-30 123浏览
-
- Go保证并发安全底层实现详解
- 2023-02-24 417浏览
-
- Go语言开发保证并发安全实例详解
- 2023-01-07 328浏览
-
- Golang 手写一个简单的并发任务 manager
- 2022-12-23 367浏览
-
- Go Java 算法之字符串解码示例详解
- 2023-01-07 479浏览

