实测能说明什么
这里记录真实本地 JVM 上的合成工作。网站回放保存证据,不代表正在运行的 JVM,也不是把预期结果做成动画。配套源码和原始录制流程用于本地调试。
版本、环境与重复方式
参考实现要求 Eclipse Temurin JDK 25.0.5+8 LTS,通过带校验值的 3.3.4 only-script wrapper 使用 Apache Maven 3.9.11,compiler 插件 3.14.1、jar 插件 3.4.2。运行代码只依赖 JDK。完整套件以种子 20261113,在独立 JVM 进程执行三轮。
JSON 保留 UTC 记录时间、JDK/runtime 构建、厂商、操作系统及版本、架构和逻辑处理器数,不包含主机名、用户名、环境变量、命令行、绝对路径或个人身份。逻辑处理器数不代表独占 CPU。切换轮次展示单次观察,不构成统计置信区间。
实验没有隔离预热、绑定 CPU、随机化策略顺序、校准 fork 或控制 GC;全部启用 JFR。sleep 精度和系统调度影响耗时。结果验证该负载下的生命周期与计数关系,不能据此推断生产吞吐或严谨的微基准排名;微基准应另外使用 JMH。
线程池口径与边界
线程池 core = max = 2,使用平台线程和容量 3 的 ArrayBlockingQueue;18 个编号任务,每个合成 sleep 60 毫秒。每次 execute() 返回后延迟 2 毫秒。CallerRuns 由提交线程执行饱和任务,因此 execute() 阻塞并改变实际到达节奏,也可能在两个池线程之外增加调用方工作。
submitted 是 execute() 前提交的任务数;started、completed 都包括池线程及调用方执行。rejected 仅计入 Abort 方式真正拒绝的任务。overflow 为进入拒绝处理器次数,callerRuns 为调用方执行次数。静止后核对提交 = 完成 + 拒绝、开始 = 完成,每个任务只有一个终态;不采用静默丢弃策略。
队列深度与活跃数为状态转换时的采样。completed 发出时执行器线程可能尚未结束包装逻辑,因此活跃采样未必已归零。曲线以阶梯连接事件采样,不推断采样间的状态。p50/p95 采用完成任务提交至完成耗时的最近秩分位数;只比较延迟会忽略拒绝数和不同到达负载。
超时与取消边界
每条路径只运行一个真实线程、任务 0。屏障确认任务进入后才调用 get(timeout)。等待方捕获 TimeoutException 后记录 waiter_returned;cancel_signal 紧邻 cancel(true) 之前;工作线程 finally 记录 work_exited。每条路径以 System.nanoTime 相对自身原点计时,毫秒输出保留至微秒位,仅为格式,不代表时钟准确度。
工作期限为进入任务后 240 毫秒,等待预算为调用 get() 后 40 毫秒,不是提交后 40 毫秒。调度和记录开销会影响返回时刻。仅等待超时不会取消 Future;另外两条路径均调用 cancel(true),一个刻意吞掉 InterruptedException 到有限期限,一个捕获中断后返回。不使用 Thread.stop。Future 状态和实际工作退出分别记录。
所有等待都有界限,线程池正常退出最多等待 3 秒,再中断清理最多 1 秒,外层套件另有 25 秒硬超时。退出失败会让实验失败;daemon 后备避免坏实验无限占住自己的 JVM。
重试失败模型与计数
12 个原始请求串行执行。偶数 ID 按种子失败 0–2 次,奇数 ID 在保存记录中失败六次,下一次尝试成功。各策略独立以 java.util.Random、种子 20261113 重建相同序列。失败概率不随时序或队列压力变化,不能称为真实雪崩故障复现。
每次尝试实际 sleep 2 毫秒。不重试策略只尝试一次;无次数上限策略一直尝试至成功,但外部每请求 2 秒安全看门狗触发即失败。有限失败确保参考实验可退出,没有测试永久故障。有限退避策略最多尝试三次,等待 5、10 毫秒,加由请求 ID、尝试编号和种子生成的 0–3 毫秒抖动。
原始请求 = 成功 + 失败。attempts 包括初次调用和全部重试;放大倍数 = 尝试 / 原始请求。backoffActualMs 累加实际 sleep 时间,不是计划延迟或总请求耗时。持续重试成功需要更多尝试;有限策略耗尽预算后明确暴露失败。本实验不验证幂等、去重、并发、熔断或分布式截止期限。
JFR 导出与隐私
JVM 实际启动 jdk.jfr.Recording,启用自定义 lab.TaskEvent、零阈值 ThreadSleep/ThreadPark、20 毫秒周期 ExecutionSample,生成真实本地 .jfr,并通过 RecordingFile 读取。另一个独立 JVM 再次读取每份文件。短时间且主要 sleep 的负载可能没有采样事件;自定义事件和 sleep 事件必须存在。
公开 JSON 仅保留文件字节数、已读取事件数、事件覆盖时段及四类白名单事件的计数,不导出线程对象/名称、调用栈、环境事件、classpath 或原始 JFR。事件覆盖时段不是校准过的程序总运行时间。静态站与公开 Git 历史都不包含原始 JFR 或下载的本地工具。
官方参考
- ThreadPoolExecutor:队列与拒绝策略
- Future:超时和取消契约
- RecordingFile:读取真实 JFR
- 固定 Temurin 25.0.5+8 发布
- Apache Maven Wrapper
- OpenJDK JMH
虚拟线程 bulkhead 协议与独立 oracle
BulkheadLab 沿用 Temurin 25.0.5+8 LTS,仅用 JDK 类,不开 preview。每次突发的 N 个任务实际运行在虚拟线程上,逐个记录 Thread.currentThread().isVirtual()。不使用 Semaphore 时 N 个任务全部进入门闩控制的合成下游;使用 C 个许可时最多 C 个进入。控制器等所有任务进入、登记等待或被拒绝后才记录 held_snapshot,再释放门闩。获准任务真实 sleep 10 毫秒。本协议验证资源占用而非吞吐。无限制对照超过声明的容量,但不虚构服务器报错。
queued 表示 tryAcquire 失败后登记等待的任务数,包含 acquire 尚未真正 park,以及获得许可但尚未记录 dequeue 的短暂区间;它不是 Semaphore.getQueueLength。阻塞 acquire 使用公平 Semaphore,但无超时的 tryAcquire 不遵循公平设置,因此不验证任务顺序或无饥饿保证。队列有限来自本次有限突发,不是 Semaphore 独立限制等待长度。
取消路径先启动 C 个持有者,再提交一个等待者。控制器在门闩关闭时观察到 35 毫秒 Future.get 超时,先取消等待任务并等待其真实 finally,再取消持有者 0。协作工作退出;故意忽略中断的对照记录中断后再次等待门闩。cancel_snapshot 分别报告 Future.isCancelled 和实际退出的 latch。释放门闩后剩余工作结束。cancelled 统计观察到中断的生命周期,不保证工作在取消信号当时就结束。等待中取消不得进入下游。只有实际 acquire 成功才允许 release,且在下游实际退出后归还。
每个 latch 等待最多 3 秒,executor 正常关闭预算 2 秒,再给自身中断清理 1 秒。watchdog 或 Future 执行失败会使整轮失败;外层 runner 在 25 秒后只终止并回收自己的 JVM。不启动常驻服务。
独立 Python oracle 从 N/C 推导预期,按事件重建在途与等待任务集合,检查每个任务恰有一个终态、所有资源快照,并与真实 JFR 的场景/转换计数比对,而非只信 assertionsPassed。另一个 JVM 再读取同一 JFR,摘要必须相同。损坏许可、线程类型、资源快照、终态计数、取消状态和 JFR 的反例只用于验证检查器,不冒充真实测量。
列出的七类 JFR 事件均显式启用,阈值为零,并关闭调用栈。Pinned 事件的阈值与通常的 20 毫秒默认值不同,因此计数不意味着存在长时间 pinning 停顿。实际的开始/结束及自定义转换必须存在;未采到的 pinned 或 submit-failed 仅报告为零,不冒充观察到事故。
保存的三轮 N=12、C=3 首波峰值均为 12/3/3;等待峰值 0/9/0;拒绝数 0/0/9。每个真实 JFR 都有 44 个虚拟线程开始、44 个结束以及 239 个自定义转换。Pinned 事件分别为 3、2、0。仅凭计数不能定位成因或归因 synchronized。原始 JFR 留在本地,公开摘要不包含调用栈、线程身份、主机或路径。
JDK 年代与实验之外的问题
JEP 444 在 JDK 21 正式交付虚拟线程,建议直接限制稀缺资源而不是池化虚拟线程;其中 synchronized pinning 的描述对应那个版本。JEP 491 在 JDK 24 交付,让虚拟线程在 synchronized 构造中阻塞时释放 carrier。Java SE 25 指南 说明 native/foreign 调用相关的剩余 pinning 与虚拟线程 JFR 事件。本实验不声称所有 synchronized 都 pin,也不声称 pinning 不可能。某轮记录为 0 仅说明该轮配置与负载中未采到。
StructuredTaskScope 在 JDK 25 仍为 preview 阅读延伸,未启用实验或 --enable-preview。本负载不含网络、数据库、真实 LLM,不比较吞吐、供应商限流、队列内存或模型质量。AI 服务类比仅帮助理解在途资源的所有权。