JDK21 虚拟线程详解【结合源码分析】
前言
JDK 21虚拟线程(Virtual Threads)是Java并发编程领域的重大突破,它通过用户态线程(轻量级线程)实现M:N调度模型,彻底解决了传统平台线程在IO密集型场景下的性能瓶颈。虚拟线程以近乎零成本的创建和销毁机制,配合JVM级别的调度优化,使得Java应用能够轻松实现百万级并发请求处理,同时保持代码的可读性和调试友好性。本文将深入解析JDK 21虚拟线程的架构设计、核心组件实现机制,并结合源码分析其挂起与恢复过程,帮助开发者全面理解这一革命性特性。
另附笔者的另一篇文章:JDK21虚拟线程和 Golang1.24协程的比较
一、虚拟线程架构设计与M:N调度模型
1.1 传统平台线程的局限性
Java传统线程(平台线程)与操作系统线程一一对应(1:1),存在以下主要问题:
- 创建成本高:每个平台线程默认占用约1MB栈内存,JVM中线程数超过几千就可能导致内存溢出(Out Of Memory)或抖动。
- 阻塞致命:当线程执行IO操作、同步锁或
sleep()
时,整个平台线程会被阻塞,无法执行其他任务。 - 线程数量受限:平台线程数量受限于操作系统线程的上限,而现代硬件资源远未达到这一限制。
1.2 虚拟线程的M:N调度模型
JDK 21虚拟线程采用M:N调度模型,大量虚拟线程(M)可映射到少量平台线程(N),其中M通常远大于N 。这一模型的核心优势在于: - 轻量级创建:虚拟线程初始栈空间仅4KB,可根据需要弹性扩展,创建和销毁几乎无成本。
- 非阻塞特性:当虚拟线程遇到阻塞操作时,不会阻塞整个平台线程,而是将任务状态保存,平台线程可立即处理其他任务。
- 资源高效利用:通过JVM级别的协作式调度,最大化利用CPU资源,提高系统吞吐量 。
虚拟线程的M:N调度模型在底层实现上与平台线程的1:1模型形成鲜明对比:
特性 | 平台线程 | 虚拟线程 |
---|---|---|
内存开销 | 约1MB/线程 | 初始4KB,弹性扩展 |
上下文切换 | 内核级,成本高 | 用户级,成本极低 |
阻塞处理 | 线程挂起,无法复用 | 自动挂起/恢复,资源可复用 |
调度方式 | 操作系统抢占式调度 | JVM协作式调度 |
适用场景 | CPU密集型任务 | IO密集型任务 |
1.3 虚拟线程架构组件
JDK 21虚拟线程架构主要包括以下核心组件:
- VirtualThread:虚拟线程的API入口,继承自
java.lang.Thread
,提供与传统线程一致的接口。 - Continuation:负责保存和恢复虚拟线程执行状态的轻量级结构,是虚拟线程实现的关键。
- Scheduler:虚拟线程的调度器,基于
ForkJoinPool
实现,负责将虚拟线程任务分发给载体线程执行。 - CarrierThread:平台线程的载体,继承自
ForkJoinWorkerThread
,负责执行实际的虚拟线程任务。 - WorkQueue:任务队列,用于存储和管理待执行的虚拟线程任务,支持工作窃取机制。
这些组件协同工作,实现了虚拟线程的挂起与恢复、任务调度与执行等核心功能。
二、核心组件实现机制解析
2.1 VirtualThread类源码分析
在JDK 21中,虚拟线程通过java.lang.VirtualThread
类实现,其构造函数如下:
final class VirtualThread extends BaseVirtualThread {VirtualThread(Executor scheduler, String name, int characteristics, Runnable task) {super(name, characteristics, /*bound*/ false);Objects.requireNonNull(task);// 选择调度器if (scheduler == null) {Thread parent = Thread.currentThread();if (parent instanceof VirtualThread vparent) {scheduler = vparent.scheduler;} else {scheduler = DEFAULT_SCHEDULER;}}this.scheduler = scheduler;this.cont = new VThreadContinuation(this, task);this.runContinuation = this::runContinuation;}// 其他方法...
}
从源码可见,虚拟线程的创建不需要显式指定线程池大小,而是由JVM自动管理 。每个虚拟线程都包含一个Continuation
对象(具体实现为VThreadContinuation
),用于保存和恢复执行状态。
虚拟线程的默认调度器DEFAULT_SCHEDULER
通过以下方式实现:
private static叉JoinPool createDefaultScheduler() {叉JoinWorkerThreadFactory factory = pool -> {PrivilegedAction pa = () -> new CarrierThread(pool);return AccessController.doPrivileged(pa);};return new叉JoinPool(// 线程数配置...factory,// 其他参数...);
}
默认调度器使用ForkJoinPool
作为基础,每个载体线程(CarrierThread)实际上是一个平台线程 ,负责执行多个虚拟线程的任务。
2.2 Continuation机制实现
Continuation是虚拟线程实现的核心,它负责保存和恢复虚拟线程的执行状态。在JDK 21中,Continuation
的实现主要通过VThreadContinuation
类完成:
final class VThreadContinuation implements Continuation {// 状态保存相关字段...private final VirtualThread thread;private final Runnable task;VThreadContinuation(VirtualThread thread, Runnable task) {this thread = thread;this task = task;// 初始化状态...}@Overridepublic void yield() {// 挂起当前执行状态...// 将当前线程状态保存到堆内存...// 通知调度器...}@Overridepublic void run() {// 恢复执行状态...// 将保存的线程状态从堆内存复制到平台线程栈...// 执行任务...try {task.run();} finally {// 处理线程结束...}}// 其他方法...
}
Continuation的yield()
和run()
方法是虚拟线程状态转换的核心:
- yield()方法:在阻塞操作发生时调用,将当前虚拟线程的执行状态(包括方法调用栈、局部变量等)保存到堆内存,释放当前载体线程资源。
- run()方法:恢复虚拟线程执行,从堆内存加载保存的执行状态到载体线程栈,继续执行任务。
2.3 Scheduler与任务调度
虚拟线程的调度器Scheduler
基于ForkJoinPool
实现,通过工作窃取(Work Stealing)算法优化任务分发 :
public class ForkJoinPool {// 任务队列相关实现...static final class WorkQueue extends Object {// 任务存储结构...// 任务窃取相关方法...boolean tryStealTask(WorkQueue wq, int max) {// 从其他队列窃取任务...// 更新任务执行状态...return true;}}// 载体线程实现...static final class CarrierThread extends叉JoinWorkerThread {private final WorkQueue workQueue = new WorkQueue();public CarrierThread(ForkJoinPool pool) {super(pool);// 初始化工作队列...}public void run() {try {// 执行虚拟线程任务...while (true) {// 获取并执行任务...WorkQueue wq = workQueue;Continuation task = wq.popTask();if (task != null) {task.run();}}} finally {// 处理线程结束...}}}
}
调度器的核心工作包括:
- 任务提交:将虚拟线程任务提交到调度器的任务队列中。
- 任务分发:将任务分配给空闲的载体线程执行。
- 工作窃取:当载体线程本地队列为空时,从其他载体线程的队列中窃取任务执行。
2.4 挂载(mount)与卸载(unmount)机制
虚拟线程与载体线程的交互通过挂载和卸载机制实现:
final class VirtualThread extends BaseVirtualThread {private void runContinuation() {mount(); // 挂载到载体线程try {cont.run(); // 执行任务} finally {unmount(); // 卸载}}private void mount() {// 将Continuation的堆栈帧复制到平台线程栈...// 建立线程与载体的关联...}private void unmount() {// 将当前执行状态保存到Continuation...// 断开线程与载体的关联...}
}
挂载操作(mount):将虚拟线程的Continuation
堆栈帧复制到载体线程的栈中,建立虚拟线程与载体线程的关联。
卸载操作(unmount):在虚拟线程需要挂起时,将当前执行状态保存回Continuation
的堆栈帧中,断开与载体线程的关联,释放载体线程资源。
三、虚拟线程挂起与恢复过程源码分析
3.1 挂起(yield)触发机制
当虚拟线程执行阻塞操作时,JVM会自动触发挂起机制。以Thread.sleep()
为例,其源码实现如下:
public static void sleep(long nanoseconds) throws InterruptedException {// 检查中断状态...// 转换为Continuation的yield操作...if (isVirtualThread()) {Continuation.yield(); // 调用Continuation的yield方法} else {// 平台线程的sleep实现...}
}
在虚拟线程中,sleep()
被重写为调用Continuation.yield()
,这使得虚拟线程的阻塞操作不会阻塞整个平台线程 ,而是将当前虚拟线程状态保存,载体线程可以立即处理其他任务。
3.2 Continuation状态保存与恢复
Continuation的挂起与恢复过程是虚拟线程实现的关键:
final class VThreadContinuation implements Continuation {// 堆栈帧数据结构...private StackFrame frame;@Overridepublic void yield() {// 保存当前执行状态...saveState();// 通知调度器...schedule();// 挂起当前虚拟线程...park();}@Overridepublic void run() {// 恢复执行状态...restoreState();// 继续执行任务...resumeTask();}private void saveState() {// 将当前线程的栈状态复制到 Continuation 的堆栈帧...// 包括方法调用、局部变量、程序计数器等...}private void restoreState() {// 从 Continuation 的堆栈帧复制状态到载体线程栈...// 恢复方法调用、局部变量、程序计数器等...}private void schedule() {// 将Continuation添加到调度器的任务队列...// 调度器会负责在适当时候重新调度该任务...}private void park() {// 挂起当前虚拟线程...// 释放载体线程资源...}// 其他方法...
}
状态保存机制:当虚拟线程需要挂起时,saveState()
方法将当前线程的执行状态(包括方法调用栈、局部变量等)序列化到Continuation
的堆栈帧中,这一过程通过JVM内部的堆栈复制实现,避免了传统线程切换的内核开销。
状态恢复机制:当虚拟线程被重新调度时,restoreState()
方法将保存的执行状态从Continuation
的堆栈帧中反序列化到载体线程的栈中,恢复线程执行到阻塞点之后 ,实现无缝的线程恢复。
3.3 调度器任务分发与恢复流程
调度器的任务分发与恢复过程是虚拟线程高效运行的核心:
public class ForkJoinPool {// 任务队列相关实现...static final class WorkQueue extends Object {// 任务存储结构...private final ArrayBlockingQueue tasks;// 载体线程执行任务...public void executeTask(Continuation task) {// 将任务添加到队列...tasks.add(task);// 如果当前载体线程没有任务执行,尝试从其他队列窃取任务...if (isIdle()) {tryStealTask();}}// 窃取任务...private void tryStealTask() {// 从其他WorkQueue窃取任务...// 如果窃取成功,执行任务...Continuation stolenTask = stealTaskFromOtherQueue();if (stolenTask != null) {stolenTask.run();}}// 任务调度...private void scheduleTask(Continuation task) {// 将任务添加到队列...tasks.add(task);// 通知调度器...// 如果当前线程有空闲载体线程,立即执行...if (hasFreeCarrierThread()) {executeTaskNow(task);}}}// 载体线程执行循环...public void run() {try {while (true) {// 获取并执行任务...Continuation task = workQueue.popTask();if (task != null) {task.run();} else {// 尝试窃取任务...workQueue.tryStealTask();}}} finally {// 处理线程结束...}}
}
调度器的任务分发与恢复流程如下:
- 任务提交:虚拟线程任务被提交到调度器的任务队列中。
- 任务分发:调度器将任务分配给空闲的载体线程执行。
- 任务执行:载体线程执行虚拟线程任务,直到任务完成或需要挂起。
- 任务挂起:当任务需要挂起时,执行状态保存到
Continuation
,载体线程继续处理其他任务。 - 任务恢复:当阻塞操作完成时,任务被重新调度,载体线程从
Continuation
加载执行状态并继续执行。
四、虚拟线程与平台线程的交互机制
4.1 载体线程(CarrierThread)实现
载体线程继承自ForkJoinWorkerThread
,负责执行虚拟线程任务:
final class CarrierThread extends叉JoinWorkerThread {private final WorkQueue workQueue;public CarrierThread(ForkJoinPool pool) {super(pool);// 初始化工作队列...workQueue = new WorkQueue();}public void run() {try {// 执行虚拟线程任务...while (true) {Continuation task = workQueue.popTask();if (task != null) {task.run();}}} finally {// 处理线程结束...}}// 载体线程与虚拟线程关联方法...public void associateWithVirtualThread(VirtualThread thread) {// 设置当前载体线程关联的虚拟线程...// 确保执行状态正确保存...}// 载体线程与虚拟线程解关联方法...public void disassociateWithVirtualThread() {// 清除当前载体线程关联的虚拟线程...// 准备执行下一个任务...}
}
载体线程与虚拟线程的交互主要通过以下方式实现:
- 关联方法:当虚拟线程挂载到载体线程时,调用
associateWithVirtualThread()
方法建立关联。 - 解关联方法:当虚拟线程需要挂起或结束时,调用
disassociateWithVirtualThread()
方法断开关联。 - 任务执行:载体线程通过
run()
方法循环执行任务队列中的虚拟线程任务。
4.2 同步操作与固定(pinning)机制
在虚拟线程中执行同步操作时,JVM会采用固定机制(pinning)确保线程安全:
public final class VirtualThread extends Thread {// 同步操作固定机制...private void pin() {// 将虚拟线程固定到当前载体线程...// 阻塞操作不会触发yield...}private void unpin() {// 解除虚拟线程与载体线程的固定关联...// 恢复正常调度...}// synchronized方法调用...public static void synchronized(Runnable task) {VirtualThread thread = currentThread();if (thread.isVirtual()) {thread pin(); // 固定虚拟线程}try {task.run();} finally {if (thread.isVirtual()) {thread unpin(); // 解除固定}}}
}
固定机制(pinning):当虚拟线程执行synchronized
块或方法时,JVM会将该虚拟线程固定(pinning)到当前载体线程,确保在同步操作期间不会被挂起或切换到其他线程 ,保证线程安全。
4.3 异步操作与非阻塞机制
虚拟线程对IO等阻塞操作的处理机制如下:
public class VirtualThreadIO {// 非阻塞IO操作封装...public static void read(Reader reader, char[] buffer) {// 检查是否需要挂起...if (isVirtualThread()) {// 注册回调,当数据可读时恢复执行...registerCallback(() -> {// 数据可读时恢复执行...resumeReading(reader, buffer);});Continuation.yield(); // 挂起当前虚拟线程} else {// 平台线程的阻塞IO实现...reader.read(buffer);}}// 恢复读取...private static void resumeReading(Reader reader, char[] buffer) {// 继续执行读取操作...// 将结果保存...// 恢复虚拟线程执行...}
}
异步非阻塞机制:当虚拟线程执行IO操作时,JVM会自动将其转换为非阻塞操作,并在数据就绪时通过回调机制恢复执行,避免了平台线程的阻塞 。
五、虚拟线程的源码实现与优化
5.1 虚拟线程创建与执行流程
虚拟线程的创建与执行流程如下:
// 手动创建虚拟线程
Thread virtualThread = Thread.ofVirtual().name("virtualThread-").unstarted(() -> {// 任务代码...System.out.println("Task started");try {Thread.sleep(Duration.ofSeconds(1));} catch (InterruptedException e) {Thread.currentThread().interrupt();}System.out.println("Task completed");});
virtualThread.start();
// 自动创建虚拟线程
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {IntStream.range(0, 10_000).forEach(i -> {executor.submit(() -> {// 任务代码...Thread.sleep(Duration.ofSeconds(1));return i;});});
}
从源码可见,虚拟线程的创建几乎无成本,可以按需大量创建 ,而无需担心资源耗尽。
5.2 虚拟线程状态管理
虚拟线程的状态管理通过以下方式实现:
final class VirtualThread extends BaseVirtualThread {private enum State { NEW, RUNNABLE,BLOCKED, Terminal, JOINED,INTERRUPTED }private volatile State state = State NEW;// 状态转换方法...private void transitionToBlocked() {// 将状态设置为堵塞...// 触发 Continuation.yield()...}private void transitionToTerminal() {// 将状态设置为Terminal...// 清理资源...}// 其他状态管理方法...
}
虚拟线程的状态包括:
- NEW:新创建但尚未启动的状态。
- RUNNABLE:可运行或正在运行的状态。
- BLOCKED:因阻塞操作而挂起的状态。
- Terminal:任务已完成或中断的状态。
5.3 调度优化与性能提升
JDK 21虚拟线程的源码实现包含多种优化机制:
- 工作窃取(Work Stealing):载体线程在本地队列空闲时,会从其他载体线程的队列中窃取任务执行,最大化利用CPU资源 。
- 零拷贝上下文切换:通过JVM内部的优化,虚拟线程的上下文切换避免了传统线程切换的拷贝开销。
- 线程本地存储优化:对
ThreadLocal
等线程本地存储的实现进行了优化,支持大量虚拟线程的高效使用 。
这些优化机制使得虚拟线程在IO密集型场景下能够显著提升系统吞吐量。
六、使用建议与最佳实践
6.1 适用场景
虚拟线程特别适合以下场景:
- 大量IO阻塞等待任务:如数据库查询、网络请求等。
- 大批量处理时间较短的计算任务。
- 需要高并发但低延迟的Web服务。
6.2 使用注意事项
使用虚拟线程时需要注意:
- 无需池化虚拟线程:虚拟线程资源开销极小,可以按需创建,无需考虑池化问题。
- 避免在虚拟线程中执行长时间运行的计算任务:虚拟线程更适合短时任务,长时间运行的任务应考虑使用平台线程。
- 注意同步操作的固定机制:执行
synchronized
块或方法时,虚拟线程会被固定到当前载体线程,应尽量减少固定时间。 - 正确处理中断:虚拟线程支持中断机制,但需要正确处理。
七、总结与展望
JDK 21虚拟线程通过用户态线程实现M:N调度模型,彻底解决了传统平台线程在IO密集型场景下的性能瓶颈 ,使得Java应用能够轻松实现百万级并发请求处理。其核心组件VirtualThread
、Continuation
、Scheduler
和CarrierThread
协同工作,实现了高效的线程挂起与恢复机制。
从源码分析可以看出,虚拟线程通过Continuation保存执行状态,避免了传统线程切换的内核开销 ;通过ForkJoinPool实现的调度器,支持工作窃取算法,最大化利用CPU资源;通过载体线程(CarrierThread)实现平台线程的高效复用。
虚拟线程的出现标志着Java并发编程进入了一个新纪元 ,使得开发者可以继续使用命令式编程风格,同时获得接近响应式编程的性能优势。随着JDK 21的正式发布,虚拟线程将成为Java高并发应用的标准解决方案,为Java生态带来革命性的性能提升。
未来,虚拟线程可能会进一步优化,特别是在CPU密集型任务的处理上,以及与现有框架(如Spring、Netty)的深度集成。开发者应积极了解和掌握这一新技术,以应对日益增长的并发需求。