中 进阶
虚拟线程#
一句话答案#
虚拟线程(JDK 21 正式,Project Loom)是 JVM 调度的轻量级线程,阻塞时自动从载体线程上卸载、不占用 OS 线程,让”一请求一线程”的同步代码也能扛住海量并发 IO。
核心要点
平台线程 vs 虚拟线程:
| 维度 | 平台线程(传统 Thread) | 虚拟线程(Virtual Thread) |
|---|---|---|
| 映射 | 1:1 映射到 OS 内核线程 | 多个虚拟线程复用少量 OS 线程(M:N) |
| 成本 | 每个约 1MB 栈,创建/切换贵 | 极轻量,栈在堆上按需增长,可创建百万级 |
| 调度 | OS 内核调度 | JVM 调度 |
| 阻塞代价 | 阻塞=占着 OS 线程白等 | 阻塞时自动卸载,释放 OS 线程去跑别的虚拟线程 |
核心机制——载体线程(carrier thread)与挂载/卸载:
- 虚拟线程运行时需”挂载(mount)“到一个平台线程(载体线程,来自 ForkJoinPool)上执行
- 遇到阻塞操作(如网络 IO、
sleep)时,JVM 把虚拟线程的栈帧保存到堆并”卸载(unmount)“,载体线程立刻去跑其他虚拟线程 - 阻塞结束后再重新挂载到某个载体线程继续执行
- 效果:阻塞的是廉价的虚拟线程,而非昂贵的 OS 线程
为什么能提升吞吐:
- 传统模型 IO 密集场景下,平台线程大量时间阻塞在 IO 上,OS 线程数受限(几千个就吃不消)→ 并发上不去
- 虚拟线程让阻塞”几乎免费”,可以放心写同步阻塞风格的代码(一请求一线程),不必转向复杂的响应式/异步回调,就能获得接近异步的吞吐
用法:
// 直接起一个虚拟线程
Thread.startVirtualThread(() -> doWork());
// 每个任务一个虚拟线程的 Executor(不需要池化!)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> handleRequest());
}java关键注意点(高频追问):
- 不要池化虚拟线程:虚拟线程本身就廉价、用完即弃,池化反而失去意义;线程池是为复用昂贵的平台线程而生
- pinning(固定)问题:在
synchronized块内或调用本地方法(JNI)时发生阻塞,虚拟线程无法卸载,会把载体线程”钉住(pinned)“,退化成平台线程的行为 → 高并发下应改用ReentrantLock替代synchronized - 不改变 CPU 密集场景:虚拟线程解决的是 IO 阻塞导致的线程浪费,对纯 CPU 计算没有提升(瓶颈是核数,不是线程数)
- ThreadLocal 仍可用但需谨慎:百万虚拟线程各自持有 ThreadLocal 会放大内存占用,详见ThreadLocal原理与内存泄漏
面试回答(2分钟版)
虚拟线程是 JDK 21 正式推出的特性,来自 Project Loom,是由 JVM 调度的轻量级线程。传统的平台线程是 1:1 映射到 OS 内核线程的,每个要占 1MB 左右的栈,创建和切换都贵,OS 线程数也就几千个的量级。虚拟线程是 M:N 模型,很多虚拟线程复用少量 OS 线程,栈放在堆上按需增长,可以轻松创建上百万个。核心机制是这样:虚拟线程要执行时会挂载到一个载体线程上,载体线程来自一个 ForkJoinPool;一旦遇到 IO 阻塞,JVM 就把它的栈帧保存到堆里、从载体线程上卸载,载体线程马上去跑别的虚拟线程,阻塞结束再重新挂载。这样阻塞的是廉价的虚拟线程而不是昂贵的 OS 线程,所以 IO 密集场景下能用简单的一请求一线程同步写法就扛住海量并发,不用再写复杂的响应式异步代码。要注意几点:虚拟线程不要去池化,它本来就用完即弃;在 synchronized 块里阻塞会发生 pinning,把载体线程钉住没法卸载,所以推荐用 ReentrantLock 替代;还有它只解决 IO 阻塞,对纯 CPU 计算没有帮助。
追问与易错
追问方向:
- “虚拟线程和平台线程什么关系?”→ M:N,多个虚拟线程复用少量平台线程(载体线程)
- “为什么阻塞不浪费资源?”→ 阻塞时栈帧存到堆、虚拟线程从载体线程卸载,OS 线程去跑别的
- “虚拟线程要不要用线程池?”→ 不要,它本身廉价用完即弃,用 newVirtualThreadPerTaskExecutor
- “什么是 pinning?怎么避免?”→ synchronized/JNI 内阻塞导致无法卸载,改用 ReentrantLock
- “虚拟线程能提升 CPU 密集任务吗?”→ 不能,只解决 IO 阻塞带来的线程浪费
易错点:
- ❌ “虚拟线程比平台线程跑得更快”——单任务不会更快,提升的是高并发 IO 下的整体吞吐
- ❌ “虚拟线程也应该池化复用”——恰恰相反,应每任务一个、用完即弃
- ❌ “用了虚拟线程 synchronized 没影响”——会 pinning,高并发下应换 ReentrantLock