在传统Java线程模型中,每个线程都要占用操作系统的线程资源。创建与销毁线程都会消耗时间和系统资源,线程上下文切换也带来了额外的成本。
当需要处理大量并发任务时,线程数量急剧上升,导致系统资源的浪费和整体性能的下降。
线程池通过复用已有线程,减少了频繁的创建与销毁,从而提升程序的性能和响应速度。
不过,任务的IO操作和并发锁常常引发线程的等待和阻塞,进而产生上下文切换,造成CPU资源的浪费,影响应用性能。尤其是在IO密集型服务中,CPU与IO的速度不匹配,使得CPU资源遭遇更大浪费。
什么是虚拟线程?它和平台线程、内核线程的区别是什么?
虚拟线程是 JDK 21+ 正式提供的轻量级用户态线程,由 JVM 调度,不直接绑定 OS 线程。
◦ 内核线程:OS 管理,重量级,数量有限。
◦ 平台线程:绑定内核线程,Java 传统线程。每个线程对应操作系统的真实线程,创建成本高,占用内存大
◦ 虚拟线程:依附平台线程。很小、动态扩容,不绑定 OS 线程,调度在用户态,资源消耗极低,所以能轻松创建百万
• 传统平台线程:线程一阻塞,内核线程直接被占死,全程不释放,干等着。
• 虚拟线程:用户态阻塞感知,只要虚拟线程一阻塞,载体平台线程马上脱身复用,没有空闲浪费。
虚拟线程是否共享栈?是否共享程序计数器?
不共享。每个虚拟线程有独立栈、独立程序计数器,只是很小、很轻。
虚拟线程的调度模型是什么?谁来调度?
虚拟线程采用 M:N 调度:M 个虚拟线程映射到 N 个平台线程。
由 JVM 的 ForkJoinPool 调度器 调度。
虚拟线程的核心设计目标是什么?解决了什么问题?
目标:高并发、低内存、代码风格不变。
解决:传统线程太重、高并发下内存占用大、IO密集任务下,CPU利用率低
核心组件
虚拟线程(VirtualThread)
JVM层面实现,java.lang.Thread子类,不直接绑定OS线程。
极轻量:默认栈几百字节、动态扩容,创建/销毁几乎零成本。
自身无执行能力,必须挂载到载体线程才能运行。
载体线程(Carrier Thread)
即传统平台线程,1:1对应OS线程,是虚拟线程的执行载体。
默认数量=CPU核心数(ForkJoinPool),同一时间只跑一个虚拟线程。
调度器(Scheduler)
默认:ForkJoinPool(优化版),管理就绪虚拟线程队列。
负责虚拟线程的挂载(mount)、卸载(unmount)、调度复用。
Continuation(续体)
本质:执行状态快照,保存程序计数器、局部变量、操作数栈、调用栈。
作用:让虚拟线程可暂停/恢复,像游戏存档/读档。
虚拟线程栈:堆上分段栈,挂起时拷贝到堆,恢复时再加载
完整运行流程
提交:虚拟线程start(),进入就绪队列。
挂载:调度器分配空闲载体线程,虚拟线程开始执行。
阻塞检测:遇到IO、sleep、Lock等阻塞点,JVM识别并触发卸载。
保存续体:将当前执行状态封装为Continuation存入堆内存。
卸载:虚拟线程从载体线程解绑,载体线程立即释放。
载体复用:载体线程从队列取下一个虚拟线程执行。
等待完成:底层IO以非阻塞方式进行,JVM注册回调。
重新挂载:IO完成后,虚拟线程重回队列;载体空闲时,恢复续体、继续执行。
虚拟线程适合什么场景?不适合什么场景
• 适合:IO 密集型(HTTP、DB、RPC、文件等)。
• 不适合:CPU 密集型,和传统线程无优势。
虚拟线程能不能替代线程池?为什么?
虚拟线程不需要池化,每次直接 new 即可。
高并发 IO 场景下,可以替代传统线程池。
使用虚拟线程时,为什么不建议使用线程局部变量(ThreadLocal)?
虚拟线程大量创建,ThreadLocal 会导致内存占用飙升,且生命周期难管理。建议用 ScopedValue。
为什么官方不建议对虚拟线程使用池化?
虚拟线程本来就是用完即丢、极轻量,池化反而增加复杂度,没有意义。
如何创建、启动、运行虚拟线程?写出两种方式。
// 方式1
Thread.startVirtualThread(() -> {});
// 方式2
Thread vt = Thread.ofVirtual().unstarted(() -> {});
vt.start();
Synchronized 会不会导致 pinning?为什么?
会。虚拟线程进入 synchronized 后会被钉住,阻塞时无法卸载,平台线程被占用。
monitor 实现把"锁的拥有者"记成了载体线程(OS 线程),而不是虚拟线程本身。为了保证 synchronized 的互斥语义不被破坏,JVM 只能禁止虚拟线程在 synchronized 块内卸载(unmount),于是载体线程被一起拖住。
如果允许虚拟线程在 synchronized 块里阻塞并 unmount:
锁 owner 记录的是 carrier A
虚拟线程 V1 卸下,carrier A 被调度去跑虚拟线程 V2
V2 因为跑在 carrier A 上,JVM 会认为"carrier A 持有这个 monitor"
V2 就能进同一个对象的 synchronized 方法 / 调用 monitorexit 释放锁
虚拟线程执行阻塞 IO 时,底层是怎么做到不阻塞平台线程的?
JVM 对 NIO 做了虚拟线程感知:
阻塞时 → 保存线程现场 → 卸载虚拟线程 → 平台线程去做别的 → IO 完成后重新挂载运行。
虚拟线程抛出异常时,栈信息有什么特点?
虚拟线程栈会包含挂载/卸载的完整调用链,dump 时能看到完整逻辑栈,方便排查。
虚拟线程的Continuation是什么作用?
Continuation 是虚拟线程的核心组件,负责保存/恢复执行现场,实现挂起和恢复。
虚拟线程是用户态调度还是内核态调度?
用户态调度,不陷入内核,开销极低。
为什么虚拟线程能极大提高 IO 密集型服务的吞吐量?
线程极轻量,阻塞时能快速切换,CPU 几乎不会空闲,连接数、吞吐量大幅提升。
生产环境直接把所有平台线程换成虚拟线程,可能会遇到什么问题?
◦ synchronized 导致 pinning,性能反而下降
◦ ThreadLocal 内存暴涨
◦ 依赖线程池、线程数量的代码逻辑异常
你线上用虚拟线程遇到过什么坑?怎么排查的?
遇到过 synchronized 导致 pinning,并发上不去。
◦ 改成 ReentrantLock 解决,因为 ReentrantLock 不会钉住虚拟线程。
什么情况下虚拟线程不但不提升性能,反而更慢?
◦ 大量 synchronized / JNI
◦ CPU 密集
◦ 深度递归、大栈帧
你怎么监控、观测、Dump 虚拟线程?
jstack、jcmd 可以看到虚拟线程
JFR 监控虚拟线程的挂载、卸载、pinning 事件