登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  java教程

Java StructuredTaskScope 怎样收集部分成功结果

来源:17golang原创

时间:2026-11-06 20:02:43 398浏览 收藏

当一组并发子任务中有几项成功、几项失败时,StructuredTaskScope.open() 的默认策略并不会自动给你一份“成功项列表”:默认策略面向的是全部成功或任一失败。要收集部分成功结果,应为作用域提供自定义 StructuredTaskScope.Joiner,在 onComplete 中只接收 SUCCESS 状态,再让 join() 返回收集结果。

官方地址:https://openjdk.org/jeps/505

本文按当前 Java SE 27 API 的预览写法说明。StructuredTaskScope 和 Joiner 属于预览 API,编译与运行时需要启用预览开关;不同 JDK 版本的 Joiner 泛型和异常签名可能不同,升级时要以对应版本的官方 API 为准。

StructuredTaskScope 中成功与失败子任务进入 onComplete 收集器并形成成功结果列表的结构说明图
图1:成功与失败子任务进入自定义 Joiner 的静态结构说明图,不是运行截图或执行证据。

先判断你要的是哪一种结果

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 PartialSuccessJoiner
        implements 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 对应不同返回结果的策略对比说明图
图2:三个 Joiner 策略与返回结果的关系说明图,不是官方界面截图。

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。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>