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;
// 初始化状态...
}
@Override
public void yield() {
// 挂起当前执行状态...
// 将当前线程状态保存到堆内存...
// 通知调度器...
}
@Override
public 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;
@Override
public void yield() {
// 保存当前执行状态...
saveState();
// 通知调度器...
schedule();
// 挂起当前虚拟线程...
park();
}
@Override
public 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)的深度集成。开发者应积极了解和掌握这一新技术,以应对日益增长的并发需求。
本文来自博客园,作者:NeoLshu,转载请注明原文链接:https://www.cnblogs.com/neolshu/p/19120470

浙公网安备 33010602011771号