虚拟线程-JDK21
虚拟线程 · JDK 21
🏠 返回 README | ⬅️ 上一篇:07-线程池与 ForkJoin | ➡️ 下一篇:98-面试高频题满分答
风格说明:本篇为 机制型 —— 沿「一次 HTTP 请求在虚拟线程上 mount → 阻塞 IO 时 unmount → carrier 复用 → pinning 时退化」的物理路径展开;覆盖 M:N 调度、Continuation、钉住语义、ScopedValue、Spring Boot 3.2+ 落地与连接池账本。
前置阅读:
07-线程池与ForkJoin.md(平台线程池七参数、拒绝策略)、04-synchronized全解.md(monitor 与锁升级)、01-线程基础与生命周期.md§8(虚拟线程入门速览)。
后续展开:10-调优排障与事故复盘.md(JFR 黄金 60s)、06-AQS与显式锁.md(ReentrantLock 与 pin 对比)、98-面试高频题满分答与Checklist.md(全套件勾选)。
L1 · 是什么:虚拟线程与 M:N 模型
1.1 一句话定义
虚拟线程(Virtual Thread):由 JVM 调度、栈帧存放在堆上的轻量线程;大量虚拟线程映射到少量 Carrier(载体)平台线程 上执行,阻塞 IO 时 unmount 释放 carrier,从而在 thread-per-request 模型下把同步阻塞代码写出接近异步的吞吐。
1.2 与平台线程的本质差异
| 维度 | 平台线程(Platform Thread) | 虚拟线程(Virtual Thread) |
|---|---|---|
| OS 映射 | 1:1 内核线程 | M:N,JVM 用户态调度 |
| 典型创建成本 | ~1 MB 栈(堆外 mmap)+ pthread_create |
初始 ~200–500 B 堆对象,按需 StackChunk 增长 |
| 数量级 | 单机 2k–8k 常见上限(ulimit、内存) |
压测可达 百万级 挂起态(受 -Xmx 约束) |
| 阻塞语义 | 占满一个 OS 线程 | 可 unmount,carrier 服务其他 VT |
| 是否池化 | 必须(ThreadPoolExecutor) |
不要池化(newVirtualThreadPerTaskExecutor) |
| 调试 | jstack 成熟 |
JDK 21+ jcmd Thread.dump_to_file -format=json |
电商交易语境:支付网关、订单查询、风控规则引擎多为 IO 密集(HTTP 渠道、DB、Redis、MQ)。JDK 21 之前用「200 线程池 + 异步编排」换吞吐;JDK 21 后可用 每请求一条虚拟线程 + 同步 JDBC,把复杂度从 Reactor 链收回到可读的 try/catch 栈。
1.3 为什么不是「协程」而是 Thread
虚拟线程仍实现 java.lang.Thread API:Thread.currentThread()、ThreadLocal(慎用)、中断、优先级(几乎忽略)。迁移成本远低于 Kotlin 协程或 WebFlux 全链路改造 —— 这是 Loom 选 Thread 语义而非全新类型的工程原因。
L2 · 原理:Carrier、Mount/Unmount、Continuation
2.1 M:N 调度总览
默认 Carrier 池规模:与 可用处理器数 相关(实现细节随 JDK 小版本调整;规划时按 CPU 核数 ≈ 活跃 carrier 上限 估算即可)。关键直觉:虚拟线程再多,同时跑在 CPU 上的载体线程仍受核数限制;unmount 释放的是 等待 IO 时不占 carrier 的能力。
2.2 Mount / Unmount 时序
Mount:虚拟线程绑定到某个 carrier,在 carrier 的原生栈上执行当前栈顶帧(热路径)。
Unmount:在 BlockingQueue.take、Socket.read、CountDownLatch.await 等 JVM 已知的 parking 点,把剩余栈帧序列化到堆上的 StackChunk,carrier 立即去跑队列里下一个就绪虚拟线程。
量化直觉(某 8C16G 支付回调服务,JDK 21.0.2,压测口径):
| 模型 | 并发请求 | P99 延迟 | CPU 利用率 | 备注 |
|---|---|---|---|---|
| 平台线程池 200 | 200 | ~180 ms | ~35% | 排队在 Tomcat 队列 |
| 虚拟线程 per-request | 5000 | ~42 ms | ~62% | 无平台线程排队;瓶颈转到渠道 RTT |
| 虚拟线程 + synchronized 内 HTTP | 5000 | ~950 ms | ~88% | pinning 退化,≈ 8 carrier 被占满 |
2.3 Continuation:可暂停的计算
底层抽象是 jdk.internal.vm.Continuation(开发者不直接 new,由 VirtualThread 封装):
- enter:在 carrier 上开始或恢复执行;
- yield:在 parking 点保存 IP + 栈帧到
StackChunk; - unmount/mount 由运行时与载体调度器协作完成。
与 OS 线程切换对比:一次 unmount 无内核态线程切换,主要是堆上对象拷贝 + 队列操作;典型 数十纳秒~微秒级(随栈深度变化),远小于 ms 级上下文切换。因此 1 万次/秒 量级的短任务切换,虚拟线程仍划算,但 纯 CPU 循环 不会变快。
2.4 创建与执行器(生产写法)
// 推荐:每任务一条虚拟线程,不要池化
try (ExecutorService exec = Executors.newVirtualThreadPerTaskExecutor()) {
exec.submit(() -> paymentChannelClient.post(payload)); // 阻塞即 unmount
}
// Spring Boot 3.2+ 见 §5.2 — 通常只需 spring.threads.virtual.enabled=true
L2 · Pinning:何时无法 Unmount
3.1 钉住(Pinning)定义
Pinning:虚拟线程在 无法安全卸载栈 的代码区域阻塞,导致 carrier 被独占,M:N 退化为「少量平台线程被占满」,吞吐与延迟回到平台线程模型甚至更差(调度开销仍在)。
3.2 三类钉住源(JDK 21 GA)
| 类型 | 典型代码 | 原因(文档化口径) | 缓解 |
|---|---|---|---|
| Monitor pin | synchronized 块内 socket.read() |
传统 monitor 与载体线程绑定实现 | 改 ReentrantLock;缩小临界区;JDK 24+ JEP 491 改善部分场景 |
| Native pin | JDBC 驱动 JNI、FileInputStream、旧版 HTTP 客户端 |
native 栈不可卸载 | 换 纯 Java NIO 驱动;升级库版本 |
| JNI 自定义 | 自研 .so 内 pthread_mutex + 阻塞 |
外来阻塞不可感知 | 避免在 VT 上调用;隔离到平台线程池 |
// ❌ 支付签名校验 + 渠道 HTTP 放在同一 synchronized
synchronized (signLock) {
String body = httpClient.send(request, BodyHandlers.ofString()); // pin carrier
verifySign(body);
}
// ✅ 锁只保护 CPU 段;IO 在锁外
String body = httpClient.send(request, BodyHandlers.ofString());
synchronized (signLock) {
verifySign(body);
}
3.3 观测:tracePinnedThreads 与 JFR
# 开发/预发:完整栈(生产可用 short 降低噪音)
-Djdk.tracePinnedThreads=short
# JFR 30s 抓取(JDK 21+)
jcmd <pid> JFR.start name=vtPin settings=profile duration=60s filename=/tmp/vt-pin.jfr
jfr print --events jdk.VirtualThreadPinned /tmp/vt-pin.jfr
| 事件 / 开关 | 用途 | 开销 |
|---|---|---|
jdk.tracePinnedThreads |
日志定位类+行号 | 低(仅 pin 时打印) |
jdk.VirtualThreadPinned (JFR) |
与 CPU、锁事件关联 | 中(短时开启) |
Thread.dump_to_file -format=json |
全量 VT 状态、carrier 映射 | 快照,秒级 |
告警建议:pin_rate = pinned_samples / virtual_thread_schedules,> 1% 持续 5 min 触发 P2;与 HikariCP active 打满 同时出现时常为 双重瓶颈(见 §4.2)。
L3 · 边界:ScopedValue、ThreadLocal、何时不用
4.1 ThreadLocal 在百万虚拟线程上的账本
假设每个请求 ThreadLocal 持有 2 KB 上下文(traceId、商户号、风控快照):
| 并发挂起 VT | ThreadLocal 堆占用 | 风险 |
|---|---|---|
| 10_000 | ~20 MB | 可接受 |
| 100_000 | ~200 MB | Full GC 压力上升 |
| 1_000_000 | ~2 GB | OOM 或 GC STW > 200 ms |
架构师规则:虚拟线程场景 禁止 大对象 ThreadLocal;InheritableThreadLocal 会触发子线程拷贝,在 newVirtualThreadPerTaskExecutor 下成本更高。
4.2 ScopedValue vs ThreadLocal
private static final ScopedValue<PaymentContext> CTX =
ScopedValue.newInstance();
ScopedValue.where(CTX, ctx, () -> {
virtualThreadExecutor.submit(() -> {
var c = CTX.get(); // 结构化继承,无拷贝
ledgerService.post(c);
});
}); // scope 结束自动失效 — 无泄漏
| 维度 | ThreadLocal | ScopedValue |
|---|---|---|
| 可变性 | 可变 set() |
不可变 绑定 |
| 生命周期 | 手动 remove() |
词法作用域 结束即清理 |
| 子线程继承 | InheritableThreadLocal 拷贝 |
StructuredTaskScope / fork 显式继承 |
| 虚拟线程 | 易 OOM | 推荐 |
| JDK 状态 | GA 已久 | 21 Preview → 后续 LTS GA(以发行说明为准) |
支付场景:用 ScopedValue 传 不可变 PaymentContext(订单号、租户、幂等键);可变 状态放请求对象或 DB,不要塞进 ThreadLocal map。
4.3 连接池耗尽:新的「线程池」瓶颈
旧世界:Tomcat max=200 → Hikari maxPoolSize=50 → 最多 50 条 SQL 在飞
新世界:VT 5000 并发 → 仍只有 50 连接 → 4950 条在 park 等连接
| 指标 | 健康 | 事故态 |
|---|---|---|
hikaricp.connections.active |
< 80% max | = max 持续 > 2 min |
http.server.requests P99 |
< 80 ms | > 500 ms 且 DB 无慢查询 |
| VT 数(JFR) | 与 QPS 线性 | > 20× CPU 核数 且 CPU 不高 |
推荐三板斧(某聚合支付核心库实践口径):
- 信号量 限制 DB 并发 =
min(DB_max_connections × 0.7, 200); - 调大 Hikari 仅在有 DBA 签字 的前提下(MySQL
max_connections硬顶); - 有界队列 在入口拒绝(Tomcat
maxConnections/ 网关限流),避免无限堆积 VT。
private final Semaphore dbInflight = new Semaphore(180);
void query() {
dbInflight.acquireUninterruptibly();
try {
jdbcTemplate.query(...);
} finally {
dbInflight.release();
}
}
4.4 明确「不该用虚拟线程」的场景
| 场景 | 原因 | 替代 |
|---|---|---|
| CPU 密集(加密、压缩、规则引擎全量扫描) | 不减少 CPU 工作;调度开销仍在 | 固定平台线程池,大小 ≈ 核数 |
| 严格背压 / 削峰 | VT 无内置 request(n) |
有界队列 + 平台池或 Reactive |
| 长生命周期任务(消费 MQ 单线程拉取) | 不需要 per-request 线程 | 专用平台线程 + 批量 |
| 大量 synchronized + 阻塞 IO 遗留库 | Pinning | 先 JFR pin 审计 再切 VT |
| ThreadLocal 泛滥的遗留框架 | 堆爆炸 | 先治理 TL 再开 VT |
4.5 平台线程池 vs 虚拟线程执行器(对照总表)
| 决策点 | ThreadPoolExecutor |
newVirtualThreadPerTaskExecutor |
|---|---|---|
| 任务类型 | CPU / 混合 / 需排队 | IO 密集、短阻塞 |
| 并发上限 | maxPoolSize + 队列 |
下游连接池 + Semaphore |
| 拒绝策略 | CallerRuns / Abort 明确 | 入口限流 + 超时 |
| 线程命名/监控 | 成熟 | JFR + JSON dump |
| 栈内存 | 堆外 -Xss |
堆内 -Xmx 要留余量 |
| Spring Boot | @Async 自定义池 |
spring.threads.virtual.enabled=true |
L4 · 架构师落地:Spring Boot、可观测、事故复盘
5.1 Spring Boot 3.2+ 配置清单
# application.yml — Servlet 栈(Tomcat 10.1+ / 11+)
spring:
threads:
virtual:
enabled: true # Boot 3.2+ 一键切换请求处理为虚拟线程
server:
tomcat:
threads:
max: 200 # 平台线程仍限制 acceptor;VT 处理业务
| 检查项 | 配置 / 代码 | 说明 |
|---|---|---|
@Async |
指定 taskExecutor 为 VT 或独立平台池 |
默认仍是平台池 |
RestTemplate / JdbcTemplate |
阻塞 API 可保留 | 优势所在 |
synchronized Service |
静态扫描 + JFR | 见 §3 |
| Actuator | metrics 暴露 jvm.threads.virtual 等 |
视 Micrometer 绑定 |
| 回滚 | spring.threads.virtual.enabled=false |
无需改业务代码 |
WebFlux 共存:同一应用可 MVC 模块 VT + WebFlux 网关 分流;勿在同一请求链混用阻塞 JDBC 于 Netty 事件循环。
5.2 可观测性仪表盘(最低集)
| 指标 | 阈值示例 | 动作 |
|---|---|---|
jdk.VirtualThreadPinned 计数 |
> 100/min | 查 synchronized/native |
| HTTP P99 | 基线 × 3 | 分 DB / 渠道 / pin |
Hikari pending threads |
> 50 | Semaphore 或扩容 |
| GC pause | ZGC < 10 ms P99 | 若 G1 STW > 50 ms 考虑 ZGC |
| Carrier CPU | 长期 > 85% | 减少 pin 或缩临界区 |
6. 真实事故复盘(电商交易场景)
- S (Situation):某聚合支付 回调接入层(JDK 21 + Spring Boot 3.2,虚拟线程已开)大促峰值 ~12k QPS 入站;下游 6 个渠道 HTTP + 订单库 Hikari max=80;SLO 回调处理 P99 < 120 ms。
- T (Trigger):大促开始后 18 min,监控 HTTP P99 从 65 ms → 1.8 s;错误率 0.02% → 3.5%(超时);oncall 收到 Tomcat 线程池无积压但 RT 飙高 告警。
- A (Approach):
- 黄金 60s:CPU 58%(不高)→ 排除纯 CPU;DB 慢查询 无新增;
jcmd Thread.dump_to_file -format=json:大量 VT WAITING on Hikari getConnection;- 并行开
-Djdk.tracePinnedThreads=short预发复现:发现ChannelRouter在synchronized内调用HttpClient.send; - JFR 60s:
jdk.VirtualThreadPinned~420 次/min,与 P99 恶化时间对齐; - 连接池:
active=80(打满),pending~3400 VT。
- R (Resolution):
- 止血(8 min):特征开关
spring.threads.virtual.enabled=false回退平台线程池(200),P99 回落 ~220 ms,错误率 < 0.1%; - 根治(3 天):
synchronized改为 仅包裹路由表查找;HTTP 移到锁外;加Semaphore(160)限制 DB 并发;Hikari 经 DBA 评估调到 120;预发 JFR pin < 5/min 后再次开启 VT。
- 止血(8 min):特征开关
- M (Metrics):VT 修复后大促峰值 P99 52 ms;同等硬件 可承载入站 12k → 15k QPS(+25%);回退窗口资损相关超时订单 ~1.1 万笔 进入补偿队列(已幂等修复)。
- P (Prevention):
- CI 静态规则:禁止 synchronized 方法内调用
HttpClient/RestTemplate; - 发布门禁:预发 10 min JFR,
VirtualThreadPinned必须为 0; - 架构评审:VT 上线 checklist(连接池、Semaphore、ScopedValue、pin)写入
10-调优排障与事故复盘.md链接; - 仪表盘:Hikari active + pin 计数 + VT 数 三合一屏。
- CI 静态规则:禁止 synchronized 方法内调用
7. 真实面试现场题(5 道带公司风格标记)
7.1 🟦 字节:「虚拟线程为什么比平台线程池更适合 IO 密集?底层发生了什么?」
(1) 标准答案
结论:IO 密集下,平台线程把 OS 线程 浪费在阻塞等待上,线程数成为吞吐天花板;虚拟线程在阻塞点 unmount,少量 carrier 即可驱动海量并发,把等待从「占线程」变成「占堆对象」。适合 thread-per-request 的支付网关、查询服务;不适合 CPU 密集或需要强背压的场景。
(2) 原理 walk
- 平台线程
read()阻塞 → 内核调度 1:1 线程睡眠 → 该 OS 线程 无法服务其他请求; - 线程池用 200 线程 ≈ 最多 200 条并发阻塞 IO;
- 虚拟线程
read()→ JVM parking → 栈进StackChunk→ carrier 释放; - carrier 数 ≈ CPU 核数级,可同时 挂载 不同 VT 执行字节码;
- IO 完成 → VT 就绪 → 再 mount 到空闲 carrier;
- 因此 5000 并发请求只需 ~5000 个堆对象 + 8 个 carrier,而非 5000 个
pthread。
(3) 权衡与量化数字
- 平台线程栈:~1 MB × 200 = 200 MB 堆外,切换 ~1–10 μs 级内核参与;
- 虚拟线程挂起:~0.5–2 KB/线程 起,百万 VT ~2 GB 堆 量级(随栈深度升);
- 某 8C 服务压测:平台池 200 P99 ~180 ms vs VT P99 ~42 ms(渠道 RTT 30 ms 主导时);
- 代价:
-Xmx需上调 15–25%;GC 扫描StackChunk增多,推荐 ZGC。
(4) 落地清单
- 开关:
Executors.newVirtualThreadPerTaskExecutor()或 Springspring.threads.virtual.enabled=true; - 监控:JFR
jdk.VirtualThread#count、http.server.requestsP99; - 配置:
-Xmx上调;不要setMaximumPoolSize池化 VT; - 回滚:
spring.threads.virtual.enabled=false。
(5) 追问
- Q:和 NIO 多路复用一个线程扛一万连接比呢?
A:Reactor 单线程事件循环 零线程切换,极限延迟更低,但 代码全异步、JDBC 不友好。VT 是 用线程抽象换开发效率,在 阻塞 API 存量 大的支付系统更现实;可混用:入口 VT + 内部 Netty 客户端。 - Q:carrier 满了会怎样?
A:就绪 VT 排队;若大量 pinning,carrier 被占满等价于 ~8 线程 服务器,P99 暴涨 —— 必须先消 pin。 - Q:ForkJoinPool 和 Tomcat 线程关系?
A:Tomcat 3.2+ 可用 VT 处理请求;acceptor 仍平台线程;业务阻塞在 VT 上 unmount,不占用 200 平台 worker。
7.2 🟧 阿里:「线上 P99 突然飙高,CPU 不高,已开虚拟线程,你怎么查?」
(1) 标准答案
优先怀疑 三类瓶颈:连接池/下游等待、pinning 导致 carrier 耗尽、GC/ThreadLocal 堆压力。按「指标分流 → JFR/JSON dump → 改 synchronized/限流」顺序,不要先加机器。
(2) 原理 walk
| 现象组合 | 根因 | 验证命令 |
|---|---|---|
Hikari active=max,无慢 SQL |
池耗尽 | jstack / dump 见 getConnection |
VirtualThreadPinned 高 |
pin | -Djdk.tracePinnedThreads=short |
| Old GC 频繁,TL 条目多 | ThreadLocal OOM | heap dump ThreadLocalMap |
| CPU 高 | 非 VT 强项 | profiler 火焰图 |
步骤:(1) 看 DB/Redis/MQ 延迟 (2) jcmd <pid> Thread.dump_to_file -format=json (3) 开 60s JFR profile (4) 对照发布 diff 是否新增 synchronized IO。
(3) 权衡与量化数字
- 连接池:VT 5000 并发 + Hikari 50 → 4950 等待,P99 可由 50 ms → 秒级;
- Pinning:8 carrier 全 pin → 吞吐 ≤ 8 并发 IO,与 200 平台池相比可能 更差;
- JFR 开销:60s profile 磁盘 ~50–200 MB,生产可接受。
(4) 落地清单
- 告警:
hikaricp.connections.active / max > 0.9持续 2 min; - 限流:
Semaphore= DB 上限 70%; - 开关:回滚 VT < 5 min;
- 门禁:预发 pin 事件 = 0 才上线。
(5) 追问
- Q:为什么 CPU 不高也很慢?
A:线程(含 VT)在 park,不算 CPU;时间在等连接/锁/pin。 - Q:jstack 够吗?
A:JDK 21 对 VT 支持有限,JSON dump + JFR 更全。
7.3 🟪 蚂蚁:「支付链路要求幂等键、租户号贯穿子调用,ThreadLocal 还是 ScopedValue?」
(1) 标准答案
选 ScopedValue 传 不可变 请求上下文;禁止 大对象 ThreadLocal。子调用用 StructuredTaskScope(或框架封装)继承绑定;幂等键写入 DB 唯一索引 兜底,不只靠线程上下文。
(2) 原理 walk
ThreadLocal.set(tenant)在每个 VT 复制 Entry → N 万 VT = N 万 Entry;InheritableThreadLocal在 fork 时 深拷贝 → 延迟更高;ScopedValue.where(KEY, immutableCtx, () -> { fork... }):无 set,子 VT 只读;- scope 结束 自动失效 → 无连接池线程 复用污染(TL 经典坑);
- 与 事务边界 对齐:scope = 单次
posting尝试。
(3) 权衡与量化数字
- TL 2 KB × 100k VT = 200 MB 仅上下文;
- ScopedValue:O(1) 绑定链,无 per-thread map;
- 迁移:先 只读 上下文,可变状态进 参数对象。
(4) 落地清单
- 代码:静态
ScopedValue<PaymentContext>; - 禁止:在线程池复用路径保留 TL;
- 测试:压测 50k VT 堆曲线应线性可控;
- 文档:标注 JDK Preview/GA 版本。
(5) 追问
- Q:和 MDC 日志兼容吗?
A:在 scope 入口MDC.putscope 出口clear,或 Log4j2 ThreadContext 与 ScopedValue 桥接;不要 在 VT 里长期挂 MDC 大 map。 - Q:跨 MQ 消费线程?
A:消息体带 tenant/idempotentKey,消费端 新建 scope 绑定,不用 TL 传递。
7.4 🟢 腾讯:「synchronized 和 ReentrantLock 在虚拟线程时代怎么选?」
(1) 标准答案
临界区内有阻塞 IO → 默认 ReentrantLock(或把 IO 移出 synchronized);仅保护内存结构、无阻塞 → synchronized 仍可用且 JIT 优化成熟。以 JFR pin 事件 验证,不以教条替代测量。
(2) 原理 walk
monitorenter路径在 VT 阻塞时 historically pin carrier;ReentrantLock.lock()阻塞走 AQS park,VT 可 unmount;- 公平锁 竞争高时仍可能拖慢,支付路径常用 非公平;
- JDK 24 JEP 491 改善部分
synchronizedpin —— 面试要报 版本号; StampedLock乐观读 无阻塞 一般不 pin。
(3) 权衡与量化数字
- Pin 场景:8 carrier 全占 → QPS 从 12k → ~800 量级(视 IO 占比);
synchronized纯内存:纳秒级,无差别;ReentrantLock:多几十纳秒 获取锁,可忽略相对 RTT 30–80 ms。
(4) 落地清单
- 静态扫描:
synchronized内禁止HttpClient、Jdbc; - JFR:
jdk.VirtualThreadPinned; - 回滚:锁拆分而非关 VT。
(5) 追问
- Q:双重检查单例?
A:volatile+synchronized构造 无 IO → 可保留;注意类加载阶段仍在平台线程。
7.5 🟡 美团:「订单查询从 Tomcat 200 线程池迁虚拟线程,你怎么做容量与回滚?」
(1) 标准答案
容量按 下游连接池与渠道 QPS 重算,不按「线程数 = 并发」;分阶段 10% → 50% → 100% 流量,预发 JFR 零 pin;回滚用 配置开关 关 VT,保留平台池参数。入口加 Semaphore 对齐 DB ~0.7×max_connections。
(2) 原理 walk
- 基线:200 平台线程,Hikari 50,峰值 QPS 3k,P99 70 ms;
- 目标:VT per-request,预期 并发等待 从 Tomcat 队列转到 堆 + IO;
- 算 Hikari:渠道 200 QPS × 3 调用 + DB 1 读 → 需 Semaphore(150–200) 而非盲目 500 连接;
-Xmx+20%(StackChunk);- 压测:8k QPS 看
active、pin、GC; - 灰度:网关权重 +
virtual.enabledprofile。
(3) 权衡与量化数字
- Tomcat 200 → 排队 ~500 ms at 3k QPS 超卖时;
- VT:排队消失,但 DB 50 连接 仍 cap ~50 并行 SQL;
- 回滚:< 5 min 配置生效,无需重新打包。
(4) 落地清单
| 项 | 值(示例) |
|---|---|
spring.threads.virtual.enabled |
true/false 开关 |
Hikari maximum-pool-size |
80→120(DBA 签字) |
Semaphore |
160 |
| JFR 门禁 | pin=0 / 10 min |
| 告警 | active/max>0.9 |
(5) 追问
- Q:还需要线程池吗?
A:CPU 任务(报表聚合)仍用 固定平台池;IO 用 VT。 - Q:CompletableFuture 还要吗?
A:可简化;复杂编排可评估 StructuredTaskScope。
8. 90 秒面试口述脚本
「JDK 21 虚拟线程是 M:N:大量 VT 映射到少量 carrier 平台线程。阻塞 IO 时 unmount 把栈存到堆上 StackChunk,carrier 去跑别的 VT,所以能用同步代码扛 上万并发 IO。三坑:synchronized/native 里阻塞会 pinning、连接池只有 50 条却开 5000 VT、ThreadLocal 百万份 OOM。生产用 Spring Boot
spring.threads.virtual.enabled,观测jdk.tracePinnedThreads+ JFRVirtualThreadPinned,上下文用 ScopedValue。CPU 密集、强背压别用 VT。」
9. 关联文件 + 一句话速记
| 文件 | 速记 |
|---|---|
07-线程池与ForkJoin.md |
平台池七参数;VT 取代 IO 池 不取代 CPU 池 |
04-synchronized全解.md |
monitor 语义;VT 下 临界区别塞 IO |
06-AQS与显式锁.md |
ReentrantLock 减轻 pin;仍怕 高竞争 CAS |
10-调优排障与事故复盘.md |
JFR 黄金 60s + pin/连接池 playbook |
01-线程基础与生命周期.md |
VT 入门与 WebFlux 决策树 |
98-面试高频题满分答与Checklist.md |
全套件勾选 Q19/Q20 |
🧭 章节导航
| # | 文件 | 主题 |
|---|---|---|
| 00 | README | 学习路径与矩阵 |
| 01 | 线程基础与生命周期 | 状态机、ThreadLocal 入门 |
| 02 | JMM 与内存可见性 | happens-before、MESI |
| 03 | volatile 深度解析 | 可见性、DCL |
| 04 | synchronized 全解 | 锁升级、monitor |
| 05 | CAS 与原子类 | ABA、Atomic 族 |
| 06 | AQS 与显式锁 | ReentrantLock、AQS |
| 07 | 线程池与 ForkJoin | 七参数、ForkJoin |
| 13 | 本篇 | 虚拟线程 JDK 21 架构师实战 |
| 08 | 并发集合与协作原语 | CHM、BlockingQueue |
| 09 | 并发设计模式与编码 | 舱壁、手写同步器 |
| 10 | 调优排障与事故复盘 | jstack、JFR、事故 |
| 11 | 分布式并发 | 分布式锁、幂等 |
| 12 | 大厂面试速查 | 五家风格速查 |
| 98 | 面试高频题满分答 | Checklist + 满分答 |
| 99 | 附录 | 命令、JDK 演进 |
📌 本篇风格说明(再强调):机制型长文,核心路径用 flowchart + sequenceDiagram + stateDiagram 表达;面试长答与 STAR-M-P 已内嵌,勿与
01§8 重复背诵,本篇用于 Staff 深挖与上线 checklist。
官方文档与源码(一级依据)
Java Concurrency · 正文机制应来自下方 官方文档(L1) 与 官方源码仓库(L2);
禁止用教程站/博客充当机制依据。本章 QPS/延迟/STAR 为面试示意。
写作规范:docs/official-sources-registry.md §0
L1 · 官方文档
L2 · 官方源码
更多推荐



所有评论(0)