前言

JDK 21 虚拟线程降低的是阻塞式 I/O 的线程占用成本,并不会凭空增加 CPU、连接池或下游服务容量。本文说明虚拟线程和结构化并发的适用边界,并给出 pinning、线程 Dump、JFR 与依赖指标结合的生产排查路径。

虚拟线程解决什么问题

平台线程直接映射操作系统线程,创建和阻塞的成本较高。虚拟线程由 JVM 调度到少量载体(carrier)平台线程上,遇到大多数 JDK 可感知的阻塞 IO 时可卸载,让载体线程继续运行其他虚拟线程。

flowchart LR
    V1[虚拟线程 1: HTTP 等待] --> C[少量载体平台线程]
    V2[虚拟线程 2: DB 等待] --> C
    V3[虚拟线程 3: CPU 计算] --> C
    C --> OS[操作系统线程]

它的价值是让大量阻塞型、每请求一线程的任务不再需要为等待占用等量 OS 线程。它不是让 CPU、数据库连接、远程服务 QPS 或内存无限增长。

1
2
3
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
Future<String> result = executor.submit(this::callRemoteService);
}

平台线程与虚拟线程怎么选

任务类型 建议 原因
大量阻塞 HTTP/数据库调用 虚拟线程可优先评估 等待期间可释放载体线程
CPU 密集计算 有界平台线程池 CPU 核数仍是硬上限
需要严格控制下游并发 虚拟线程 + Semaphore/连接池 虚拟线程不会替你限制数据库连接
长生命周期后台消费者 按框架与吞吐模型评估 需要显式背压、批处理和关闭治理
使用大量 native 调用/旧阻塞库 先压测和观测 可能无法友好卸载

pinning 风险

虚拟线程在 synchronized 临界区内执行阻塞操作,或进入某些 native/foreign 调用时,可能无法从载体线程卸载,称为 pinning。高并发下大量 pinning 会耗尽载体线程,使虚拟线程失去优势。

治理不是机械地把所有 synchronized 改成 Lock,而是缩短临界区,禁止在锁内做 IO;借助 JFR 观察 pinned virtual thread 事件;升级已修复兼容问题的库和 JDK。JDK 版本演进会改善部分场景,但锁内阻塞本身仍是坏设计。

结构化并发

结构化并发把一组相关子任务作为一个作用域管理:父任务取消时子任务取消;一个关键子任务失败可让整体提前失败;结果等待与资源释放在同一代码块内完成。它能让并发请求的生命周期更清晰。使用时要注意具体 JDK 版本的预览状态和项目的发布策略,不能把预览 API 当作所有生产环境的固定依赖。

并发故障排查路径

现象 首要证据 常见根因
CPU 持续高 多次 jstack、JFR、CPU profiler 自旋 CAS、死循环、序列化、过量上下文切换
请求卡住 线程 Dump、慢 SQL、连接池指标 锁等待、下游超时未设、连接池耗尽
吞吐下降且队列上涨 线程池 active/queue/reject、下游延迟 消费慢、无界积压、线程池隔离失败
死锁 jstack 的 deadlock 检测 加锁顺序不一致、嵌套锁
虚拟线程很多仍变慢 JFR pinning、DB/HTTP 连接指标 载体被 pin、下游容量不足、CPU 饱和

排查应连续采样,而不是只看一次线程 Dump。先确认线程是在 RUNNABLE 消耗 CPU、BLOCKED 等锁、WAITING 等队列,还是卡在网络/数据库调用;再将线程名、TraceId、线程池和下游指标串起来。

常用工具:jcmd <pid> Thread.printjstack <pid>、Java Flight Recorder(JFR)、jcmd <pid> JFR.start、应用指标与链路追踪。线上抓取前应评估频率与权限,避免高频 Dump 造成额外压力。

高频面试题

虚拟线程能替代线程池吗?

不能一概而论。对于大量阻塞式 HTTP、数据库或文件 I/O 任务,可以使用“每任务一个虚拟线程”减少平台线程被等待占用的成本;但 CPU 密集任务仍需要有界的执行器,任务队列、生命周期管理和隔离策略也依然需要。虚拟线程改变的是线程调度成本,不是并发治理本身。

虚拟线程适合 CPU 密集型任务吗?

通常不适合用它来提升 CPU 密集计算的吞吐。CPU 核数仍是硬上限,创建更多虚拟线程只会增加调度和竞争。应按 CPU 核数设置有界并行度,必要时拆分批处理、削峰或把计算迁移到专门的计算服务。

什么是 pinning,如何排查?

当虚拟线程在 synchronized 临界区内执行阻塞操作,或陷入部分 native/foreign 调用时,载体平台线程可能无法卸载该虚拟线程,这就是 pinning。排查时通过 JFR 采集 pinned virtual thread 事件,结合线程 Dump、锁等待栈和下游耗时定位代码路径;治理重点是缩小临界区、禁止锁内 I/O、升级存在兼容问题的 JDK 或依赖,而不是机械替换所有锁。

为什么仍需要连接池、Semaphore 和限流?

数据库连接、HTTP 客户端连接、第三方配额和下游服务容量都是有限资源。虚拟线程允许更多请求发起等待,却不会增加这些资源的供给;没有并发上限会把等待转成连接池耗尽、超时风暴和级联故障。因此仍要在下游边界配置连接池、Semaphore、超时、限流、熔断和背压。

结构化并发能直接作为生产稳定 API 吗?

先核对当前使用的 JDK 版本和项目发布策略。结构化并发能清晰表达一组子任务的取消、失败传播与等待关系,但不同 JDK 版本的 API 状态可能仍在演进。生产中应封装在边界清晰的组件内,做版本兼容验证和压测,不应把预览 API 扩散为全站基础依赖。

线上请求变慢时,如何判断是虚拟线程问题还是下游瓶颈?

先建立证据链:查看 JFR 是否存在持续 pinning,线程 Dump 中载体线程是否被锁或 native 调用占住,再关联 CPU、GC、数据库连接池、HTTP 连接池、下游延迟和超时比例。若请求主要等待数据库或远程调用、连接池已满或下游延迟升高,根因是容量或依赖问题;只有在 pinning 或载体线程耗尽证据明确时,才把重点放在虚拟线程实现。

面试收束

JDK 21 虚拟线程主要优化高并发阻塞 IO 的线程成本,不能突破 CPU 与下游资源限制。生产使用仍要保留连接池、Semaphore、超时、限流和背压。并发故障要通过线程状态、队列、锁、CPU 和下游指标建立证据链,而不是只靠增加线程数。