当前位置: 首页 > news >正文

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 staticJoinPool createDefaultScheduler() {JoinWorkerThreadFactory factory = pool -> {PrivilegedAction pa = () -> new CarrierThread(pool);return AccessController.doPrivileged(pa);};return newJoinPool(// 线程数配置...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 extendsJoinWorkerThread {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 {// 处理线程结束...}}
}

调度器的任务分发与恢复流程如下:

  1. 任务提交:虚拟线程任务被提交到调度器的任务队列中。
  2. 任务分发:调度器将任务分配给空闲的载体线程执行。
  3. 任务执行:载体线程执行虚拟线程任务,直到任务完成或需要挂起。
  4. 任务挂起:当任务需要挂起时,执行状态保存到Continuation,载体线程继续处理其他任务。
  5. 任务恢复:当阻塞操作完成时,任务被重新调度,载体线程从Continuation加载执行状态并继续执行。

四、虚拟线程与平台线程的交互机制

4.1 载体线程(CarrierThread)实现

载体线程继承自ForkJoinWorkerThread,负责执行虚拟线程任务:

final class CarrierThread extendsJoinWorkerThread {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应用能够轻松实现百万级并发请求处理。其核心组件VirtualThreadContinuationSchedulerCarrierThread协同工作,实现了高效的线程挂起与恢复机制。

从源码分析可以看出,虚拟线程通过Continuation保存执行状态,避免了传统线程切换的内核开销 ;通过ForkJoinPool实现的调度器,支持工作窃取算法,最大化利用CPU资源;通过载体线程(CarrierThread)实现平台线程的高效复用。

虚拟线程的出现标志着Java并发编程进入了一个新纪元 ,使得开发者可以继续使用命令式编程风格,同时获得接近响应式编程的性能优势。随着JDK 21的正式发布,虚拟线程将成为Java高并发应用的标准解决方案,为Java生态带来革命性的性能提升。

未来,虚拟线程可能会进一步优化,特别是在CPU密集型任务的处理上,以及与现有框架(如Spring、Netty)的深度集成。开发者应积极了解和掌握这一新技术,以应对日益增长的并发需求。

http://www.dtcms.com/a/336406.html

相关文章:

  • 弹性布局 Flexbox
  • BEVFusion(2022-2023年)版本中文翻译解读+相关命令
  • Java项目架构设计:模块化、分层架构的实战经验
  • Linux(十六)——top命令详解
  • wrap go as a telnet client lib for c to implement a simple telnet client
  • 堆的实际应用场景
  • 【Virtual Globe 渲染技术笔记】8 顶点变换精度
  • C11期作业17(07.05)
  • Microsoft WebView2
  • AMBA-AXI and ACE协议详解(十)
  • Rust:DLL 输出对象的生命周期管理
  • 影刀初级B级考试大题2
  • STM32CUBEMX配置stm32工程
  • Linux学习-多任务(线程)
  • LangChain4j
  • 三分钟在VMware虚拟机安装winXP教程,开箱即用
  • HTTP0.9/1.0/1.1/2.0
  • linux下timerfd和posix timer为什么存在较大的抖动?
  • USB-A 3.2 和 USB-A 2.0的区别
  • 集成电路学习:什么是ORB方向性FAST和旋转BRIEF
  • 外贸电商选品方案的模型
  • 天地图应用篇: 增加缩放、比例尺控件
  • 集运业务突围:三大关键问题的智能化解决方案
  • 【数据结构与算法-Day 16】队列的应用:广度优先搜索(BFS)的基石与迷宫寻路实战
  • vulnhub-lampiao靶机渗透
  • 002.Redis 配置及数据类型
  • 安装pytorch3d后报和本机cuda不符
  • LLM、RAG、Agent知识点思维导图
  • 简单了解BeanFactory和FactoryBean的区别
  • AMBA-AXI and ACE协议详解(八)