Java并发:JDK21虚拟线程、结构化并发与生产排查
前言
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 | try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { |
平台线程与虚拟线程怎么选
| 任务类型 | 建议 | 原因 |
|---|---|---|
| 大量阻塞 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.print、jstack <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 和下游指标建立证据链,而不是只靠增加线程数。


