结构化并发:取消传播与有界接纳(每任务一线程的生命周期)
结论基线:正式 API 以 Java SE 21 为准(
ExecutorService.close、Future.cancel,@since 19/21),实现分析固定到 OpenJDK 21ujdk-21.0.5+9;结构化并发是预览 API(JEP 453,JDK 21 起已位于java.util.concurrent,只需--enable-preview),实现分析固定到 OpenJDK 23ujdk-23.0.2+7(与运行 JDK 23.0.2 同 tag)。本机实测 Oracle JDK 21.0.5+9 与 23.0.2+7(x86_64,macOS 26.7.1)同一探针源码双档编译运行;JDK 24/25/26 未安装,JEP 499/505/525 差异按 JEP 推导并逐项标注未实跑。本页是 U13 在 虚拟线程机制正文(调度器/pinning/单信号量容量对照,U12)之上的取消与等待增量;取消三层分离口径与池关闭协议的 C02 权威在中断协议正文与线程池正文,互链不重写。
问题:每任务一线程之后,取消与等待谁管
线程池时代,生命周期由池代管:接纳有队列、关闭有 shutdown/shutdownNow(能退回没开跑的任务)、取消有 Future.cancel 落到池线程(线程池正文)。换成每任务一线程后两个前提消失:没有队列(每次 submit 立刻开新线程),任务边界不再有池这个"管理者"——newVirtualThreadPerTaskExecutor 的 javadoc 明写 unbounded,接纳协议由调用方的并发预算回答(U12 容量段)。于是遗留两个 U12 未展开的问题:
- 取消:
close()等什么、shutdownNow还能退回什么、cancel(false)/(true)在虚拟线程上传播什么;"一组子任务要么都成功要么一起取消"谁来表达——这正是结构化并发(JEP 453 预览)的回答:任务结构=取消结构。 - 等待:下游池有界不等于系统有界——等待集必须同样有界。"线程更轻"不改变任何一条容量账单(U12 已证),本页补上等待侧:无超时的池获取在下游挂起时把单点故障放大成整池挂起。
ExecutorService 生命周期:close 的等待与升级
ExecutorService.close()(@since 19,try-with-resources 的关闭语义)是等待协议,不是取消协议:
原码连续摘录(仅统一公共缩进,截取方法局部):ExecutorService.java,jdk-21.0.5+9,L409–L429。
@Override
default void close() {
boolean terminated = isTerminated();
if (!terminated) {
shutdown();
boolean interrupted = false;
while (!terminated) {
try {
terminated = awaitTermination(1L, TimeUnit.DAYS);
} catch (InterruptedException e) {
if (!interrupted) {
shutdownNow();
interrupted = true;
}
}
}
if (interrupted) {
Thread.currentThread().interrupt();
}
}
}读这段源码得到三条可验证的边界:close() 先 shutdown() 再按天为步长循环等待,不主动中断在途任务;只有 close() 的调用线程自身被中断才升级为 shutdownNow()(对全部在途线程发 interrupt),然后继续等到底并恢复中断状态。本机实测(JDK 21,BoundedAdmissionProbe 第 1/2 节):300ms 的在途任务,close() 在 ~311ms 返回、任务 interrupted=false 正常跑完;另一相位里 closer 线程在 200ms 被中断,升级 shutdownNow 后 5 秒任务在 ~206ms 停下、closer 的中断状态被保留——开发期"close 会中断任务"的断言先红(红3),实测后转绿。工程含义:try-with-resources 包住 executor 等于"等所有任务自然结束";要"超时后取消",必须用 线程池正文的四步协议(shutdown → 有界 awaitTermination → shutdownNow → 协调在途副作用)手写,close() 没有超时参数可给。
每任务一线程没有队列:shutdownNow 的空列表
池的 shutdownNow 返回"没开跑的任务"列表(U09 已摘 jdk-17.0.12+7 的 ThreadPoolExecutor 实现)。每任务一线程的实现把这一项清零:
原码连续摘录(仅统一公共缩进,截取类定义局部):ThreadPerTaskExecutor.java,jdk-21.0.5+9,L44–L48。
/**
* An ExecutorService that starts a new thread for each task. The number of
* threads is unbounded.
*/
class ThreadPerTaskExecutor extends ThreadContainer implements ExecutorService {原码连续摘录(仅统一公共缩进,截取方法局部):ThreadPerTaskExecutor.java,jdk-21.0.5+9,L151–L163。
public void shutdown() {
checkPermission();
if (!isShutdown())
tryShutdownAndTerminate(false);
}
@Override
public List<Runnable> shutdownNow() {
checkPermission();
if (!isTerminated())
tryShutdownAndTerminate(true);
return List.of();
}shutdownNow 恒返回 List.of()——没有"未开始任务"可退(tryShutdownAndTerminate(true) 对 threads 集合逐个 interrupt,即全部在途线程收到中断请求)。本机实测:5 个 sleep(400ms) 任务全部启动后 shutdownNow,返回 0 个、5/5 观察到中断(开发期按池语义断言"能退回未开始任务"先红,红4)。接纳协议的差异由此闭合:池模型里"排队中的任务"是可处置资产(队列快照逐个登记/重跑),每任务一线程模型里队列消失了,提交前就必须被调用方预算拦住——虚拟线程正文的"接纳边界前移"在关闭侧的对应物。
cancel 的两层语义在虚拟线程上的实测
中断协议正文的三层分离(结果层/线程层/业务层)在虚拟线程上逐层成立,本页补每任务一线程模型的实测锚点(BoundedAdmissionProbe 第 4 节):cancel(false) 在结果层 CAS 赢得终态(返回 true、isCancelled()==true、get() 抛 CancellationException),线程层照常运行——sleep(1500) 的任务在 cancel(false) 后仍跑满 ~1500ms 才结束;cancel(true) 向执行线程发出 interrupt 请求,协作任务在 ~0ms 观察到 InterruptedException 退出。业务层(已发出的下游请求)与线程类型无关,仍是幂等键与对账的责任。开发期"cancel(false) 也会停线程"的断言先红(红5)。Future/FutureTask 的取消竞争细节(CAS state、interrupting 交接)权威在FutureTask/CompletableFuture 正文,本页不重复摘录。
结构化并发:结构即取消
"fork 一组子任务、要么都成功要么把没完成的取消掉"——池模型下要手写 cancel/finally 编排,结构化并发把它做成语言级结构。版本状态先行(每项状态查询于 2026-10-25,见来源与版本):
| 版本 | JEP | 状态与形态 | 本机验证 |
|---|---|---|---|
| JDK 19/20 | 428/437 | 孵化(jdk.incubator.concurrent 模块,--add-modules;fork 返回 Future;437 加 ScopedValue 继承) | 未实跑(20 的模块本机存在但 API 旧形态,不据此断言行为) |
| JDK 21 | 453 | 预览:API 移入 java.util.concurrent,--enable-preview 即可,fork 返回 Subtask | 实测(21.0.5) |
| JDK 22/23 | 462/480 | 二/三次预览,官方记载 without change | 实测(23.0.2)——同一探针源码双档编译运行一致 |
| JDK 24 | 499 | 四次预览,无变更 | 未实跑(JDK 24 未安装) |
| JDK 25 | 505 | 五次预览,API 重构:公共构造器改静态 open() 工厂、Joiner 策略接口、join() 抛 FailedException、超时进 Configuration | 未实跑,按 JEP 推导 |
| JDK 26 | 525 | 六次预览(Closed/Delivered;relates 533 七次预览) | 未实跑 |
预览 API 不进生产签名——"能编译≠正式可用"的先例(String Templates 撤回)已在语言演进页立过;--enable-preview 的版本绑定约束同 U12 版本边界表。开发期按"孵化模块"旧认知用 --add-modules jdk.incubator.concurrent 编译,21/23 双双报「找不到模块」(红1)——JEP 453 起这条旧路径已不存在,版本认知本身也要按版本核查。
取消的核心机制在类的 javadoc 里:
原码连续摘录(仅统一公共缩进,截取类注释局部):StructuredTaskScope.java,jdk-23.0.2+7,L100–L108。
* <p> {@code StructuredTaskScope} defines the {@link #shutdown() shutdown} method to shut
* down a task scope without closing it. The {@code shutdown()} method <em>cancels</em> all
* unfinished subtasks by {@linkplain Thread#interrupt() interrupting} the threads. It
* prevents new threads from starting in the task scope. If the owner is waiting in the
* {@code join} method then it will wakeup.
*
* <p> Shutdown is used for <em>short-circuiting</em> and allow subclasses to implement
* <em>policy</em> that does not require all subtasks to finish.
*shutdown() 对未完成子任务逐一 interrupt 并阻止新 fork——这与 U09 三层分离口径同构:线程层的请求,不是强杀。两个内置策略把两种业务语义落到这条机制上:
- 失败短路(
ShutdownOnFailure):任一子任务失败 → shutdown 取消兄弟 →throwIfFailed()把首个异常包成ExecutionException抛给所有者。实测:60ms 失败的任务让 4 秒的兄弟在 ~94/97ms(21/23 双档一致)被中断,整体不再等 4 秒。 - 首成功短路(
ShutdownOnSuccess):首个成功完成即 shutdown。机制在分支里:
原码连续摘录(仅统一公共缩进,截取方法局部):StructuredTaskScope.java,jdk-23.0.2+7,L1035–L1052。
protected void handleComplete(Subtask<? extends T> subtask) {
if (firstResult != null) {
// already captured a result
return;
}
if (subtask.state() == Subtask.State.SUCCESS) {
// task succeeded
T result = subtask.get();
Object r = (result != null) ? result : RESULT_NULL;
if (FIRST_RESULT.compareAndSet(this, null, r)) {
super.shutdown();
}
} else if (firstException == null) {
// capture the exception thrown by the first subtask that failed
FIRST_EXCEPTION.compareAndSet(this, null, subtask.exception());
}
}CAS 赢得"首个成功"的线程调用 super.shutdown()——输家被取消。实测(40ms winner + 两个 1.5s 输家):输家在 ~49/54ms 收到中断,整体 <1200ms。对照:CompletableFuture.anyOf 拿到首个完成后直接返回,输家照常跑完(实测输家 isCancelled()==false 且在 1.2s 后正常完成,红2 的纠正)——CF 的组合器竞争的是结果槽(U09 超时与取消不共享一条协议),不管理资源;结构化并发把"取消资源"也纳入结构。这是选择两者的架构理由:只为结果聚合选 CF,为"聚合+资源回收"选结构化并发。
close() 的等待语义(预览 API 内):
原码连续摘录(仅统一公共缩进,截取方法局部):StructuredTaskScope.java,jdk-23.0.2+7,L830–L843。
@Override
public void close() {
ensureOwner();
int s = state;
if (s == CLOSED)
return;
try {
if (s < SHUTDOWN)
implShutdown();
flock.close();
} finally {
state = CLOSED;
}close() 先把未 shutdown 的 scope 取消掉,再等所有子任务线程终止——取消是请求,收尾必须等到。两个实测边界(JDK 21/23 双档一致):所有者中断——owner 在 join 前被置位中断,join() 立即抛 InterruptedException,try-with-resources 随即取消两个 3 秒子任务(~1ms 内收到中断);不响应中断的子任务拖住 close——一个自旋 800ms、忽略中断标志继续跑的子任务,让 scope 在失败短路后仍要等到 ~803ms 才走出 try 块(红7 的纠正:取消≠立即结束,JEP 525 的文字「close always waits for threads executing subtasks to finish」与本机观察互证)。结构强制:关闭后 fork 被拒(实测 IllegalStateException);JEP 记载 scope 不实现 ExecutorService/Executor(防止非结构化复用),嵌套错序可抛 StructureViolationException(本探针未构造该场景,按 JEP 登记未实跑)。
上下文:ScopedValue 继承实测(U12 未实跑项的升级)
U12 虚拟线程正文登记过 Scoped Value 的预览轨道(446→506)且未实跑;本页在结构化并发场景把它落到实测:ScopedValue.where(CTX, "req-13").run(...) 内 scope.fork(...) 的子任务继承绑定(读到 req-13),run 返回后所有者线程解绑(orElse 回落)。JDK 21 与 23 双档一致。工程含义:结构化并发的子任务天然拿到 owner 在 fork 时点的不可变上下文,不需要 U09 capture-restore 装饰器——但该结论只在 --enable-preview 的 21–24 成立,ScopedValue 25 才由 JEP 506 正式;迁移建议照旧必须写版本区间。
有界接纳:等待必须有界
U12 容量段证明了"Semaphore 限流下两种线程模型同吞吐";本页补等待侧的账单。先固定位置:每任务一线程没有执行队列,等待发生的地点从"池的阻塞队列"移到下游资源的获取等待集(连接池的 take 等待、信号量的许可等待)。等待集里的每个等待者仍然是一条虚拟线程、仍然是一份栈 chunk——等待也吃内存,而且无超时的等待集把"下游故障"放大成"本服务全线挂起"。
| 资源 | 界放在哪 | 谁负责 | 没有界的后果 |
|---|---|---|---|
| CPU 并行 | 调度器并行度(≈CPU 数) | JVM 默认 | ——(天然有界) |
| 线程/栈内存 | 提交侧并发预算(每任务一线程无池上限) | 调用方 | 百万任务的栈 chunk 撑爆堆(U12 账单) |
| 下游连接 | 连接池大小 + 获取超时 | 池配置 + 任务侧 | 无超时=等待集无界(本节实测) |
| 下游并发 | Semaphore 许可(对齐池大小或更小) | 代码 | 并发直达下游(U12 红2) |
| 等待时长 | tryAcquire/poll(timeout) | 任务侧 | 单点挂起放大为整池挂起(本节实测) |
| 总在途 | 入口限流/背压(限流器正文、上游 accept 队列) | 架构 | 等待集本身继续膨胀 |
反例实测(连接池用 10 条"连接"的 ArrayBlockingQueue 替身模拟,非真实数据库驱动;BoundedAdmissionProbe 第 5 节):
| 相位 | 输入 | 实测(观察值) | 结论 |
|---|---|---|---|
| A 无超时·下游正常 | 100 任务 × 持有 50ms | 100/100 完成 ~599ms,maxInUse=10,maxWaiters=90 | 正常时无超时只是"排队",等待集最多 = 提交数-连接数 |
| B 无超时·下游全挂 | 10 条连接全部被挂起持有 + 30 新任务 | 600ms 内 0/30 完成 | 「只是慢」是错的(红6):等待者全部卡死,故障被放大 |
| C 有界拒绝·下游全挂 | 同 B + 获取超时 200ms | 30/30 拒绝,~215ms 收敛 | 有界失败:挂起变拒绝,系统保住恢复能力 |
| 恢复 | 中断挂起持有者 | 连接归还,B 相 30 任务全部完成 | 恢复依赖持有者响应中断——又回到协作取消 |
相位 B/C 之间就是"有界池≠有界系统"的完整论证:池大小只界住在途,等待必须由超时界住,提交总量必须由上游预算界住——三层界缺一,故障就把缺的那层打穿。开发期还遇到一个值得登记的基准陷阱(红8):初版构造成本观察直接写 new Thread(...).join(),测出平台线程 ~4µs 的"假数"——ThreadMXBean.startedDelta=0 证明线程根本没有启动(HotSpot 逃逸分析消除了不逃逸的 Thread 对象);捕获 threadId() 让对象逃逸后,真实观察值是虚拟线程 create-start-join ~31µs、平台线程 ~120–133µs(约 3–4 倍,n=1000/200,单机观察值非契约)。口径:虚拟线程的规模优势在内存与伸缩性(U12 的栈 chunk 账单),"单次构造成本更低"只是同量级的方向性观察,不能当作容量推理的依据。
设计案例:批量聚合调用的取消与预算评审(算例,非实测)
负载假设:比价服务每请求聚合 3 个上游报价(各 50–500ms 阻塞 I/O),任一报价成功即可用(其余取消);下游各限 50 并发;JDK 21 部署、可带 --enable-preview 的灰度环境。不变量:任何时刻对单一上游的在途 ≤50;总响应 p99 ≤ 800ms;取消的下游请求要有可追踪身份(业务层)。
选择 1:CompletableFuture.anyOf + 默认池。 组合器只聚合结果,输家继续跑完——对上游 2/3 的浪费流量照发(每请求 3 路全部打满),下游配额被无取消的输家占用;要取消得手写 cancel/finally 编排(U09 的取消三层逐层补),错误处理散落。超时用 orTimeout 只是完成策略,不回收线程(U09 已证)。
选择 2:StructuredTaskScope.ShutdownOnSuccess(预览)+ 每路 Semaphore(50) + 所有者 deadline。 结构自带"首成功取消输家"(实测输家 ~50ms 收到中断);每路信号量把在途压到下游配额内;总超时在 21 的 API 没有 scope 级超时(JEP 505 的 Configuration.withTimeout 在 25 才出现,未实跑),用 owner 线程 join 前置中断或给最慢子任务自带 deadline——超时层次必须显式设计,不能假设 scope 替你做了。成本:预览 API 的升级风险(505 已重构 API,21–24 的代码迁移要改写);恢复责任:被取消的上游请求可能已发出,幂等键照旧。结论:结果聚合 alone 用 CF;聚合+取消+预算成套用结构化并发,并绑定 21–24 版本区间与 --enable-preview 前提。两个选择的等待侧账单相同:信号量等待集都要超时,否则上游挂起放大照旧发生(上一节相位 B)。
条件变化与边界
| 条件变化 | 需要复核的点 |
|---|---|
| JDK 17 部署 | 虚拟线程/结构化并发都不存在;取消语义回到 U09 池模型权威 |
| JDK 20 | 结构化并发在孵化模块(旧 API:fork 返回 Future);本机模块存在但未实跑,不据此断言行为 |
JDK 21/23 + --enable-preview | 本页全部实测基线;预览 API 不进生产签名 |
| JDK 24 | JEP 499 四次预览无变更;JEP 491 消除 synchronized pinning(U12 已登记);未实跑 |
| JDK 25 | JEP 505 重构 API(open()/Joiner/FailedException)——21–24 的 ShutdownOnFailure 代码要迁移;ScopedValue 由 JEP 506 正式;未实跑 |
| 不响应中断的子任务 | 拖住 ExecutorService.close() 与 scope close——关闭协议必须先治理不响应中断的任务(U09) |
| 无超时的池/信号量获取 | 等待集无界,下游故障放大为整池挂起(相位 B);一律 tryAcquire/poll(timeout) |
| 池化虚拟线程 | 无意义(JEP 444,U12);限流/预算与线程模型解耦 |
实验:输入、步骤、预期与边界
实验入口:labs/modern-java(StructuredScopeProbe.java + BoundedAdmissionProbe.java,run.sh structured|bounded,与 U12 的 evolution|virtual|jep400 并列不互斥)。
cd <仓库根>
export JAVA_HOME21=/Library/Java/JavaVirtualMachines/jdk-21.jdk/Contents/Home
export JAVA_HOME23=/Library/Java/JavaVirtualMachines/jdk-23.jdk/Contents/Home # structured 需要
bash labs/modern-java/run.sh structured # JEP 453 预览 API:21/23 同源双档 + 取消传播 + anyOf 对照 + 孵化模块编译负例
bash labs/modern-java/run.sh bounded # JDK21 正式 API:close/shutdownNow/cancel + 连接池替身有界等待 + 构造成本输入与预期(2026-10-25 实测,完整输出与八次红绿记录见证据):
| 场景 | 输入 | 预期与实测(21/23 一致,除注明外) |
|---|---|---|
| 失败短路 | 60ms 失败 + 4s 兄弟 | 兄弟 ~94/97ms 被中断;throwIfFailed 抛 ExecutionException(cause=IllegalState) |
| 所有者中断 | join 前置中断 + 2×3s 子任务 | join 抛 InterruptedException;双子任务 ~1ms 取消 |
| 首成功短路 | 40ms winner + 2×1.5s 输家 | 输家 ~49/54ms 中断;整体 <1200ms |
| anyOf 对照 | 同构 CF 三路 | 输家 isCancelled()==false,1.2s 后正常完成(不取消) |
| 协作取消 | 800ms 自旋忽略中断 + 60ms 失败 | close ~802/803ms 才返回(取消是请求,收尾等到) |
| 结构强制 | close 后 fork | IllegalStateException |
| ScopedValue | where(CTX,"req-13") 内 fork | 子任务读到 req-13;run 后 owner 解绑 |
| close 等待 | 300ms 在途任务 | ~311ms 返回、interrupted=false |
| close 升级 | closer 在 200ms 被中断 | shutdownNow 升级;5s 任务 ~206ms 停;中断状态保留 |
| shutdownNow | 5 个在途任务 | 返回空列表、5/5 中断 |
| cancel 两层 | cancel(false)/(true) 各一 | false:结果层取消、线程跑满 1500ms;true:~0ms 中断退出 |
| 有界等待 | 10 连接替身 ×100/30/30 任务 | A:100 完成/等待 90;B:挂起 0/30;C:30 拒绝 ~215ms |
| 构造成本 | threadId 捕获后 n=1000/200 | 虚拟 ~31µs、平台 ~120–133µs(观察值非契约) |
| 编译负例 | --add-modules jdk.incubator.concurrent | 21/23 双拒(「找不到模块」,JEP 453 移入 java.util.concurrent) |
TDD 红绿(真实,不倒造):红1「21 还需要孵化模块」(--add-modules jdk.incubator.concurrent 编译失败,JEP 453 只需 --enable-preview);红2「anyOf 取消输家」(isCancelled=false 实测推翻);红3「close 中断在途任务」(任务正常跑完推翻);红4「shutdownNow 退回未开始任务」(返回 0 推翻);红5「cancel(false) 停线程」(任务照常跑完推翻);红6「无超时池等待只是慢」(下游全挂 0/30 推翻);红7「取消=立即结束」(close 等 803ms 推翻);红8「随手基准测到平台线程 4µs」(startedDelta=0——逃逸分析消除未逃逸 Thread 对象,捕获 threadId 后 79–133µs)。边界:毫秒/微秒数字均为单机观察值;连接池是替身(模型)非真实驱动;JDK 19/20/24/25/26 的结构化并发形态未实跑;StructureViolationException 场景未构造;native/FFM 与真实 HTTP 下游未纳入本探针。
追问链
第一层:close() 到底是关闭协议还是等待协议?
默认实现只有"等待+自身被中断时升级",没有超时、没有强制取消。要"超时后取消",用四步协议手写(shutdown → 有界 awaitTermination → shutdownNow → 协调副作用);close 的价值是 try-with-resources 的确定性收尾,代价是所有任务必须可终止。
第二层:结构化并发比 ExecutorService+Future 强在哪、弱在哪?
强在:子任务树与取消结构绑定(失败/首成功短路自动取消兄弟)、所有者中断自动传播、结构强制(fork-after-close 被拒)、Subtask 只能在 join 后取结果(消除对未完成结果的误读)。弱在:预览 API(21–24 需 --enable-preview,25 已重构)、没有执行队列与并发预算(限流仍要自己加信号量)、anyOf 式的结果聚合用 CF 更熟。选择按"是否需要取消回收资源"而非新旧。
第三层:为什么"池有界"不等于"系统有界"?
池只界住在途(permit/连接占用);等待集大小 = 提交速率 × 无超时等待时长,下游一挂就是全量提交数;等待者本身吃栈 chunk 内存。三层界:在途(池/信号量)、等待(获取超时)、提交总量(上游预算/背压)。相位 B 的 0/30 与相位 C 的 30 拒绝是同一系统的两个命运,差别只有 200ms 超时。
第四层:评审"上结构化并发"的方案按什么顺序问?
- 版本区间与
--enable-preview前提写了吗(505 已重构 API);2. 每路下游的并发预算与获取超时在哪(scope 不管限流);3. 不响应中断的子任务有没有(拖住 close);4. 超时层次:scope 级(25 才有)还是 owner 中断/deadline;5. 被取消请求的业务层身份(幂等键)与可观测(JFR 线程容器可看子任务树)。五问对应五类事故:版本漂移、无界下游、关不掉、超时缺失、对账无门。
来源与版本
| 来源 | 版本 | 用途 |
|---|---|---|
| JEP 453 Structured Concurrency (Preview) | JDK 21 | 预览状态、java.util.concurrent 位置、Subtask、取消传播(2026-10-25 查询) |
| JEP 462/480/499 | JDK 22/23/24 | 二/三/四次预览均 without change(2026-10-25 查询) |
| JEP 505 / JEP 525 | JDK 25/26 | 五/六次预览;505 的 open()/Joiner/FailedException 重构(推导引用,未实跑) |
| JEP 506 Scoped Values | JDK 25 | ScopedValue 正式化轨道终点;21–24 为 446/464/476/487 预览(U12 版本表承接) |
| Java SE 21 ExecutorService API | Java SE 21 | close/awaitTermination/shutdownNow 契约 |
| OpenJDK 21u ExecutorService.java | jdk-21.0.5+9 | close 等待/升级实现(L409) |
| OpenJDK 21u ThreadPerTaskExecutor.java | jdk-21.0.5+9 | unbounded javadoc(L44)与 shutdownNow 空列表(L151) |
| OpenJDK 23u StructuredTaskScope.java | jdk-23.0.2+7 | shutdown 取消语义(L100)、首成功分支(L1035)、close 等待(L830) |
本地 labs/modern-java 实验 | Oracle 21.0.5 + 23.0.2 | 本文全部行为实测(证据) |
访问日期:2026-10-25。源码行号分别以 jdk-21.0.5+9(正式 API)与 jdk-23.0.2+7(预览 API)为准,不混 tag 断言;JDK 24/25/26 差异按 JEP 推导未实跑并已逐项标注;结构化并发为预览 API,其 API 形态在 505 已变更,正文示例绑定 21–24 形态。
闭卷验收
本页 12 道闭卷题收进了下方自测面板:先回忆,再展开参考答案与评分点,最后给三档自评。