虚拟线程 · 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

JVM 调度层

mount

mount

blocking IO

VirtualThread 就绪队列

ForkJoinPool
默认 Carrier 池

carrier-1

carrier-2

carrier-N
≈ CPU 核数

StackChunk 链

Continuation 对象

virtualThread-1001

virtualThread-1002

virtualThread-1003

默认 Carrier 池规模:与 可用处理器数 相关(实现细节随 JDK 小版本调整;规划时按 CPU 核数 ≈ 活跃 carrier 上限 估算即可)。关键直觉:虚拟线程再多,同时跑在 CPU 上的载体线程仍受核数限制;unmount 释放的是 等待 IO 时不占 carrier 的能力。

2.2 Mount / Unmount 时序

内核/网卡 CarrierThread VirtualThread Tomcat请求 内核/网卡 CarrierThread VirtualThread Tomcat请求 检测到 parking 点 处理支付回调 mount 执行字节码 socketRead 阻塞 栈帧拷贝到 StackChunk unmount 释放 carrier 调度其他 VT2 IO 完成 可运行 重新 mount 继续 返回 200

Mount:虚拟线程绑定到某个 carrier,在 carrier 的原生栈上执行当前栈顶帧(热路径)。

Unmount:在 BlockingQueue.takeSocket.readCountDownLatch.awaitJVM 已知的 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 封装):

  1. enter:在 carrier 上开始或恢复执行;
  2. yield:在 parking 点保存 IP + 栈帧到 StackChunk
  3. 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

Thread.start

mounted on carrier

park at IO

unpark remount

run completes

NEW

RUNNABLE

WAITING

TERMINATED

unmounted 时无 carrier
栈在 StackChunk


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 自定义 自研 .sopthread_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 不高

推荐三板斧(某聚合支付核心库实践口径):

  1. 信号量 限制 DB 并发 = min(DB_max_connections × 0.7, 200)
  2. 调大 Hikari 仅在有 DBA 签字 的前提下(MySQL max_connections 硬顶);
  3. 有界队列 在入口拒绝(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)
    1. 黄金 60s:CPU 58%(不高)→ 排除纯 CPU;DB 慢查询 无新增
    2. jcmd Thread.dump_to_file -format=json:大量 VT WAITING on Hikari getConnection
    3. 并行开 -Djdk.tracePinnedThreads=short 预发复现:发现 ChannelRoutersynchronized 内调用 HttpClient.send
    4. JFR 60s:jdk.VirtualThreadPinned ~420 次/min,与 P99 恶化时间对齐;
    5. 连接池: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。
  • M (Metrics):VT 修复后大促峰值 P99 52 ms;同等硬件 可承载入站 12k → 15k QPS(+25%);回退窗口资损相关超时订单 ~1.1 万笔 进入补偿队列(已幂等修复)。
  • P (Prevention)
    • CI 静态规则:禁止 synchronized 方法内调用 HttpClient/RestTemplate
    • 发布门禁:预发 10 min JFRVirtualThreadPinned 必须为 0;
    • 架构评审:VT 上线 checklist(连接池、Semaphore、ScopedValue、pin)写入 10-调优排障与事故复盘.md 链接;
    • 仪表盘:Hikari active + pin 计数 + VT 数 三合一屏。

7. 真实面试现场题(5 道带公司风格标记)

7.1 🟦 字节:「虚拟线程为什么比平台线程池更适合 IO 密集?底层发生了什么?」

(1) 标准答案

结论:IO 密集下,平台线程把 OS 线程 浪费在阻塞等待上,线程数成为吞吐天花板;虚拟线程在阻塞点 unmount,少量 carrier 即可驱动海量并发,把等待从「占线程」变成「占堆对象」。适合 thread-per-request 的支付网关、查询服务;不适合 CPU 密集或需要强背压的场景。

(2) 原理 walk

  1. 平台线程 read() 阻塞 → 内核调度 1:1 线程睡眠 → 该 OS 线程 无法服务其他请求
  2. 线程池用 200 线程 ≈ 最多 200 条并发阻塞 IO;
  3. 虚拟线程 read() → JVM parking → 栈进 StackChunkcarrier 释放
  4. carrier 数 ≈ CPU 核数级,可同时 挂载 不同 VT 执行字节码;
  5. IO 完成 → VT 就绪 → 再 mount 到空闲 carrier;
  6. 因此 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() 或 Spring spring.threads.virtual.enabled=true
  • 监控:JFR jdk.VirtualThread#counthttp.server.requests P99;
  • 配置:-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 504950 等待,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

  1. ThreadLocal.set(tenant) 在每个 VT 复制 Entry → N 万 VT = N 万 Entry;
  2. InheritableThreadLocal 在 fork 时 深拷贝 → 延迟更高;
  3. ScopedValue.where(KEY, immutableCtx, () -> { fork... })无 set,子 VT 只读
  4. scope 结束 自动失效 → 无连接池线程 复用污染(TL 经典坑);
  5. 事务边界 对齐: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.put scope 出口 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

  1. monitorenter 路径在 VT 阻塞时 historically pin carrier
  2. ReentrantLock.lock() 阻塞走 AQS park,VT 可 unmount
  3. 公平锁 竞争高时仍可能拖慢,支付路径常用 非公平
  4. JDK 24 JEP 491 改善部分 synchronized pin —— 面试要报 版本号
  5. StampedLock 乐观读 无阻塞 一般不 pin。

(3) 权衡与量化数字

  • Pin 场景:8 carrier 全占 → QPS 从 12k → ~800 量级(视 IO 占比);
  • synchronized 纯内存:纳秒级,无差别;
  • ReentrantLock多几十纳秒 获取锁,可忽略相对 RTT 30–80 ms

(4) 落地清单

  • 静态扫描:synchronized 内禁止 HttpClientJdbc
  • JFR:jdk.VirtualThreadPinned
  • 回滚:锁拆分而非关 VT。

(5) 追问

  • Q:双重检查单例?
    Avolatile + synchronized 构造 无 IO → 可保留;注意类加载阶段仍在平台线程。

7.5 🟡 美团:「订单查询从 Tomcat 200 线程池迁虚拟线程,你怎么做容量与回滚?」

(1) 标准答案

容量按 下游连接池与渠道 QPS 重算,不按「线程数 = 并发」;分阶段 10% → 50% → 100% 流量,预发 JFR 零 pin;回滚用 配置开关 关 VT,保留平台池参数。入口加 Semaphore 对齐 DB ~0.7×max_connections

(2) 原理 walk

  1. 基线:200 平台线程,Hikari 50,峰值 QPS 3k,P99 70 ms
  2. 目标:VT per-request,预期 并发等待 从 Tomcat 队列转到 堆 + IO
  3. 算 Hikari:渠道 200 QPS × 3 调用 + DB 1 读 → 需 Semaphore(150–200) 而非盲目 500 连接
  4. -Xmx +20%(StackChunk);
  5. 压测:8k QPSactive、pin、GC;
  6. 灰度:网关权重 + virtual.enabled profile。

(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:还需要线程池吗?
    ACPU 任务(报表聚合)仍用 固定平台池;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 VTThreadLocal 百万份 OOM。生产用 Spring Boot spring.threads.virtual.enabled,观测 jdk.tracePinnedThreads + JFR VirtualThreadPinned,上下文用 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 · 官方源码

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐