文章中如果有图看不到,可以点这里去 csdn 看看。从那边导过来的,文章太多,没法一篇篇修改好。

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;
        // 初始化状态...
    }
    @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 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;
    @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 {
            // 处理线程结束...
        }
    }
}

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

  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)的深度集成。开发者应积极了解和掌握这一新技术,以应对日益增长的并发需求。

posted @ 2025-08-17 17:05  NeoLshu  阅读(412)  评论(0)    收藏  举报  来源