编程 Project Loom 不是一次升级:三块内容分属 JDK 21 与 JDK 25 两条时间线

2026-10-07 00:03:14

Project Loom 不是一次升级:三块内容分属 JDK 21 与 JDK 25 两条时间线

Project Loom 常被当成 JDK 里的一次并发模型升级整体端出来,但它从来不是一个单一 feature。虚拟线程、结构化并发、作用域值(Scoped Values)走的是不同的 JDK 时间线,把它们塞进同一次升级里理解,真正变化的部分反而被盖住了。

三块内容的状态

  • 虚拟线程(JEP 444):JDK 21 定稿。
  • 作用域值 Scoped Values(JEP 506):JDK 25 定稿。
  • 结构化并发(JEP 505,第五个 preview):JDK 25 仍是 preview;OpenJDK 已继续到 JEP 525(第六个预览)。
  • JEP 491:修复虚拟线程在 synchronized 块里被 pinning 的问题。

参考链接:

虚拟线程(JEP 444,JDK 21 定稿)

虚拟线程也是 java.lang.Thread 的实例,但不绑定 OS 线程。运行时调度把虚拟线程 mount 到平台线程(carrier)上,遇到阻塞 I/O 时虚拟线程 unmount,carrier 空出来去跑别的虚拟线程。

常见用法:

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> System.out.println("hello"));
}

newVirtualThreadPerTaskExecutor 不做线程池,每个提交的任务新建一个虚拟线程。注意 ThreadLocal 在虚拟线程里要慎用,因为数量可能到百万级。

结构化并发:API 形状在 JDK 25 变了

StructuredTaskScope 在 java.util.concurrent 包,把一个任务拆成若干并发子任务(subtask),要求它们在任务继续前完成。核心动作是 fork 和 join。fork 默认起一个虚拟线程。

JDK 21 的写法(旧子类模型):

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Subtask user = scope.fork(() -> fetchUser());
Subtask order = scope.fork(() -> fetchOrder());
scope.join();
scope.throwIfFailed();
return new Result(user.get(), order.get());
}

JDK 25 的写法(改用静态工厂 + Joiner):

try (var scope = StructuredTaskScope.open(Joiner.awaitAllSuccessfulOrThrow())) {
Subtask user = scope.fork(() -> fetchUser());
Subtask order = scope.fork(() -> fetchOrder());
return new Result(user.get(), order.get());
}

变化点:

  • 用静态工厂 open() 取代公开构造器和子类继承。
  • 引入 Joiner 接口,并发策略在 open 时就绑定;join() 遵循该策略,直接返回结果或抛异常,不再是先 join() 再 throwIfFailed() 两步。
  • 内置 Joiner:awaitAllSuccessfulOrThrow()(默认,全成功否则抛 FailedException)、anySuccessfulResultOrThrow()(第一个成功即返回,其余取消,对应旧 ShutdownOnSuccess)、awaitAll()(等全部完成,不抛异常,返回 null)、allUntil()(自定义谓词,收集到满足条件即停并取消剩余)。
  • 也可以自己实现 Joiner,比如「第一个完成(无论成败)即返回、其余取消」,JDK 没有内置这个策略。

一个真实迁移仓库的数字:Java 21 分支有 56 处 ShutdownOnFailure、7 处 ShutdownOnSuccess、48 处 throwIfFailed;迁到 Java 25 后变成 63 处 StructuredTaskScope.open 和 63 处 Joiner。代码没离开结构化并发,是从内置子类换成了显式 joiner 策略。

其他约束:

  • fork/join/close 只能由 owner 线程(打开 scope 的线程)调用。
  • scope 用 try-with-resources,close 会取消未完成子任务。
  • 可设超时,超时 join 抛 StructuredTaskScope.TimeoutException。
  • 子任务可以自己再开 StructuredTaskScope,形成 scope 层级;jcmd 生成的 thread dump 里能看到这棵树。
  • 结构化并发的错误会以 FailedException 传播。

作用域值(JEP 506,JDK 25 定稿)

Scoped Values 让方法在同一线程及其子线程间共享不可变数据,比 ThreadLocal 更容易推理,空间和时间开销也更低,尤其搭配虚拟线程与结构化并发。

一个实测细节:在结构化 scope 里 fork 的子任务能拿到上下文;而普通嵌套虚拟线程(非结构化)里,child 和 grandchild 打印出来是 unknown。

迁移时注意

  • pom.xml 里 source/target 从 21 抬到 25,--enable-preview 不能去掉,因为结构化并发仍是 preview。
  • 契约更明确:Java 21 代码读起来像「开一个特殊 scope,fork,join,再补一步策略调用」;Java 25 读起来像「用这个策略开 scope,fork,join 时按策略判定」。

虚拟线程让阻塞代码等待更便宜、作用域值给请求上下文一个词法形状、结构化并发还在改,因为 JDK 自己都还没敲定 ownership 模型。

推荐文章

程序员茄子在线接单