JAVA JVM虚拟线程

Java 虚拟线程是 Java 19 中引入的一项重要特性,并在 Java 21 中正式成为标准功能。它是 Java 平台对轻量级线程的实现,旨在简化高并发应用的开发,提升系统资源利用率。

传统线程的局限性

在虚拟线程出现之前,Java 依赖操作系统线程(OS Threads)实现并发,这些线程被称为平台线程(Platform Threads)。每个平台线程对应一个操作系统线程,具有以下特点:
  • 重量级:创建和销毁成本高,内存占用大(通常 1MB + 栈空间)。
  • 数量受限:系统资源限制了可创建的线程数量(如数千到数万级别)。
  • 阻塞代价高:线程阻塞时会占用系统资源,导致上下文切换开销。
这使得传统多线程应用在处理大量并发连接(如 Web 服务器)时面临性能瓶颈。

虚拟线程的优势

虚拟线程是由 Java 运行时(JVM)管理的轻量级线程,具有以下特性:

1.轻量级

        每个虚拟线程占用内存极小(约 KB 级别)。

        创建和销毁成本低,可轻松创建百万级虚拟线程。

2.高效利用资源

        虚拟线程由 JVM 调度,与操作系统线程解耦。

        当虚拟线程阻塞时,JVM 会自动切换到其他任务,避免资源浪费。

3.同步代码简化异步编程

        虚拟线程允许使用传统的synchronized、wait/notify等同步机制,无需编写复杂的异步回调代码。

虚拟线程核心概念

1.载体线程

虚拟线程运行在平台线程(称为载体线程)上,但多个虚拟线程可复用同一个载体线程。

载体线程由 JVM 的调度器管理,类似于操作系统调度进程。

2.协程(对应源码中Continuation类)

虚拟线程本质上是 Java 对协程的一种实现的官方支持,属于用户态线程。


什么是协程(补充):        

        协程(Coroutine) 是一种轻量级的线程管理机制,属于用户态线程(而非操作系统内核管理的线程)。它允许程序在不使用传统多线程的情况下实现并发执行

  • 核心概念:

(1)协作式调度

协程与传统线程的最大区别在于调度方式:
传统线程:由操作系统内核调度,线程的挂起和恢复完全由系统控制(抢占式调度)。
协程:由程序自身控制执行流程,通过主动让出执行权(yield)来实现协作式调度。

(2)轻量级

协程的创建和销毁成本极低,内存占用通常只有 KB 级别(相比之下,传统线程的栈内存默认 1MB+)。因此,一个程序可以轻松创建数百万个协程。

(3)状态保存

协程能在暂停执行时保存自身状态(如局部变量、程序计数器),并在恢复时继续执行,就像从未中断过一样。
3.结构化并发
协程能在暂停执行时保存自身状态(如局部变量、程序计数器),并在恢复时继续执行,就像从未中断过一样。

从源码角度讲解虚拟线程的原理

1.虚拟线程的创建过程:

public final class VirtualThread extends Thread {
    private final Continuation cont;  // 协程上下文
    private final Executor scheduler; // 任务调度器
    
    public VirtualThread(Runnable target) {
        super(null, null, "VirtualThread", 0, false, null);
        // 创建Continuation实例,传入执行逻辑
        this.cont = new Continuation(VM_VTHREAD_CONTINUATION_STACK, target);
        this.scheduler = VirtualThread.captureScheduler();
    }
    
    @Override
    public void start() {
        if (getState() != Thread.State.NEW)
            throw new IllegalThreadStateException();
        // 将虚拟线程提交给调度器
        scheduler.execute(this::runContinuation);
    }
    
    private void runContinuation() {
        // 执行Continuation(协程)
        cont.run();
    }
}
关键点
  • Continuation是虚拟线程的核心,它封装了线程的执行上下文(如局部变量、程序计数器)。
  • start()方法不会立即执行线程,而是将其放入调度器的任务队列。
2.Continuation 机制详解
Continuation是 JVM 提供的底层协程实现
// jdk.internal.vm.Continuation简化
public final class Continuation {
    private final Object stack;     // 协程栈数据
    private final Runnable task;    // 待执行的任务
    private ContinuationScope scope; // 协程作用域
    
    public Continuation(ContinuationScope scope, Runnable target) {
        this.scope = scope;
        this.task = target;
        this.stack = allocateStack(); // 分配协程栈(小而动态)
    }
    
    public void run() {
        if (status == INITIAL) {
            // 首次执行,进入JVM层的协程执行逻辑
            VM.continuationStart(this);
        } else if (status == SUSPENDED) {
            // 恢复执行
            VM.continuationResume(this);
        }
    }
    
    // 暂停当前协程的执行
    public static void yield() {
        VM.continuationYield(currentContinuation());
    }
}
JVM 层面的关键机制
  • VM.continuationStart():启动协程执行,保存初始上下文。
  • VM.continuationResume():恢复协程执行,从上次暂停处继续。
  • VM.continuationYield():暂停协程执行,保存当前上下文并释放载体线程。
3. 调度器与载体线程
虚拟线程的调度由ForkJoinPool实现,核心逻辑如下:
// ForkJoinPool虚拟线程调度器的简化逻辑
class ForkJoinPool implements ExecutorService {
    // 工作线程数组(载体线程)
    private final ForkJoinWorkerThread[] workers;
    
    // 提交虚拟线程任务
    public void execute(Runnable task) {
        if (task instanceof VirtualThread) {
            VirtualThread vt = (VirtualThread) task;
            // 将任务放入工作队列
            WorkQueue queue = getWorkerQueue();
            queue.push(vt);
            // 唤醒或创建载体线程执行任务
            signalWork();
        }
    }
    
    // 载体线程执行逻辑
    final void runWorker(WorkQueue w) {
        while (scan(w)) { // 扫描任务
            Runnable task = w.pop();
            if (task instanceof VirtualThread) {
                VirtualThread vt = (VirtualThread) task;
                // 执行虚拟线程的Continuation
                vt.runContinuation();
            }
        }
    }
}
调度关键点
  • Work-Stealing 算法:每个载体线程维护自己的任务队列,空闲时从其他队列 "窃取" 任务。
  • 载体线程池:默认大小为 CPU 核心数,可通过jdk.virtualThreadScheduler.parallelism参数调整。
  • 弹性伸缩:调度器会根据负载动态调整活跃载体线程数量。
4. 阻塞操作的处理
当虚拟线程遇到阻塞操作(如 IO)时,JDK 会通过 Instrumentation 机制自动转换为非阻塞逻辑
步骤
  1. 检测当前线程是否为虚拟线程。
  2. 将阻塞操作转换为非阻塞调用(如使用 NIO Selector)。
  3. 在 IO 等待期间,通过Continuation.yield()暂停虚拟线程。
  4. IO 完成后,调度器恢复虚拟线程执行。

从源码角度对传统线程与虚拟线程进行对比分析

1. 线程创建与资源占用
平台线程(Thread类)Java 平台线程直接映射到操作系统线程,由java.lang.Thread类实现。
核心源码结构
// java.lang.Thread类简化结构
public class Thread implements Runnable {
    private long nativePeer; // 指向JVM中对应的原生线程
    
    // 线程栈大小(默认由JVM决定,通常为1MB+)
    private long stackSize;
    
    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
    
    private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
        // ...
        this.stackSize = stackSize;
        // 调用JVM底层创建原生线程
        start0();
    }
    
    private native void start0(); // 调用操作系统创建线程
}
关键点
  • nativePeer字段指向 JVM 内部的原生线程结构。
  • 每个线程默认占用较大的栈空间(如 1MB),创建成本高。
  • start0()是本地方法,依赖操作系统创建线程,开销大。
虚拟线程(VirtualThread类)虚拟线程由java.lang.VirtualThread实现,是 Java 21 + 的内置类:
// java.lang.VirtualThread类简化结构
public final class VirtualThread extends Thread {
    private final Continuation cont; // 协程实现的核心
    
    public VirtualThread(ThreadGroup group, Runnable target) {
        super(group, null, "VirtualThread", 0, false, null);
        // 创建协程上下文
        this.cont = new Continuation(VM_VTHREAD_CONTINUATION_STACK, target);
    }
    
    @Override
    public void start() {
        // 将虚拟线程提交给调度器
        scheduler.submit(this);
    }
}
关键点
  • Continuation是 JVM 内部的协程实现,负责保存和恢复线程状态。
  • 虚拟线程栈内存由 JVM 管理(通常 KB 级别),创建成本极低。
  • start()方法仅将任务提交给调度器,不直接创建操作系统线程。
2. 调度机制对比
平台线程调度
平台线程的调度完全依赖操作系统:
  • 线程状态(RUNNABLE、BLOCKED 等)由操作系统内核管理。
  • 线程阻塞时(如 IO 操作),会触发上下文切换(从用户态到内核态),成本高(约 1μs~10μs)。
虚拟线程调度
虚拟线程由 JVM 用户态调度器管理:
// 虚拟线程调度器简化逻辑
class VirtualThreadScheduler {
    private final ExecutorService carrierThreads; // 载体线程池
    
    public void submit(VirtualThread vt) {
        // 将虚拟线程放入任务队列
        taskQueue.add(vt);
        // 唤醒载体线程执行任务
        carrierThreads.submit(() -> runTask(vt));
    }
    
    private void runTask(VirtualThread vt) {
        // 恢复虚拟线程上下文
        vt.cont.run();
    }
}
关键点
  • 虚拟线程阻塞时(如 IO),JVM 通过Continuation.yield()暂停执行,释放载体线程资源。
  • 载体线程可继续执行其他虚拟线程,实现多路复用,大幅提升吞吐量。
  • 调度器使用 Work-Stealing 算法优化任务分配。
3. 阻塞操作处理
平台线程的阻塞
平台线程执行阻塞操作时:
// 传统线程执行IO操作(阻塞示例)
public void readFile() throws IOException {
    try (var reader = new BufferedReader(new FileReader("data.txt"))) {
        String line;
        while ((line = reader.readLine()) != null) { // 阻塞调用
            // 处理数据
        }
    }
}
  • 线程会被挂起,等待 IO 完成,期间占用系统资源。
虚拟线程的阻塞
虚拟线程执行相同操作时:
// 虚拟线程执行IO操作
public void readFile() throws IOException {
    try (var reader = new BufferedReader(new FileReader("data.txt"))) {
        String line;
        while ((line = reader.readLine()) != null) { // 虚拟线程在此处"暂停"
            // 处理数据
        }
    } // 虚拟线程恢复执行
}
  • JVM 会将虚拟线程标记为阻塞,暂停执行并释放载体线程。
  • IO 完成后,虚拟线程会在任意可用的载体线程上恢复执行。
4. 适用场景对比

场景

平台线程

虚拟线程

IO 密集型高并发

需要复杂的异步编程

直接使用同步代码即可

CPU 密集型任务

合适(避免频繁切换)

不合适(调度开销可能更高)

资源受限环境

需谨慎控制线程数

可大量创建,自动优化资源

代码迁移成本

需重构为异步(如 CompletableFuture)

直接替换线程池即可

优劣分析
平台线程的优势
  1. 适合 CPU 密集型任务:避免频繁调度开销。
  2. 实时性强:由操作系统内核直接调度,响应速度快。
  3. 兼容性好:支持所有 Java 版本,无需特殊 API。
平台线程的劣势
  1. 资源消耗大:高并发时内存占用显著。
  2. 扩展性差:受限于操作系统线程数量上限。
  3. 编程模型复杂:处理大量并发需使用异步编程(如 Netty)。
虚拟线程的优势
  1. 高扩展性:轻松支持百万级并发连接。
  2. 简化编程:使用同步代码实现异步性能。
  3. 资源高效:内存占用低,上下文切换快。
  4. 无缝兼容:与现有线程 API(如ExecutorService)兼容。
虚拟线程的劣势
  1. JDK 版本依赖:仅 Java 21 + 支持。
  2. 调试复杂度:虚拟线程堆栈信息可能更复杂。
  3. CPU 密集型优化不足:长时间运行的任务可能导致调度器过载。

虚拟线程适用场景

  • 高并发服务器:如 Web 服务器和应用服务器,处理大量并发请求时性能卓越。
  • 微服务架构:使服务更轻量级、敏捷,提升服务间通信效率。
  • 并行计算:在数据处理和分析场景中,提供必要并行处理能力,提升计算效率。
  • 异步 I/O 操作:涉及文件或网络 I/O 的应用程序中,可提高异步操作性能。

总结

虚拟线程的高性能源于以下设计:

1.轻量级上下文切换

虚拟线程切换仅涉及用户态数据(Continuation 对象),无需内核参与。

2.栈内存优化

虚拟线程使用动态增长的小栈(通常 KB 级别),而不是固定的大栈。

3.无锁设计

调度器使用 CAS 操作和无锁队列减少竞争。

4.任务窃取

Work-Stealing 算法确保负载均衡,减少线程空闲时间。

虚拟线程本质上是:
  • 用户态线程:由 JVM 而非操作系统管理。
  • 协作式调度:通过Continuation.yield()主动让出执行权。
  • 轻量级协程:每个线程占用极小资源,支持百万级并发。
Java 虚拟线程通过轻量级设计和高效调度,显著降低了并发编程的资源成本,使开发者可以用同步代码编写高性能异步应用。它是 Java 平台应对现代高并发场景的重要改进,尤其适合 IO 密集型的大规模并发应用。
<think>我们刚刚详细讨论了Project Loom和虚拟线程,现在用户问的是“Java JVM是怎么调度虚拟线程的”。这个问题其实在之前的讲解中已经涵盖了核心部分,但需要更聚焦在调度机制上。 根据之前的回答,虚拟线程的调度核心是M:N调度模型,由JVM的调度器负责将虚拟线程(M个)映射到载体线程(N个)上执行。关键点包括: 1. 调度器:JVM内部的调度器(ForkJoinPool的变体?),不是OS内核调度器。 2. 挂起与恢复:当虚拟线程执行阻塞操作时,JVM会将其挂起(保存栈状态到堆),并释放载体线程;当阻塞操作完成,调度器会安排它在可用的载体线程上恢复执行。 3. 协作式调度:调度发生在特定的阻塞点(I/O、锁等),而不是像OS线程那样抢占式。 现在需要更详细地展开调度过程,并确保回答完整。同时,最后必须按照要求提出3-5个相关问题。 注意:用户要求用中文回答,且最后必须以“§§相关问题§§”开头的问题列表,不能换行分散。 因此,回答结构: 1. 详细解释调度机制(包括调度器、挂起恢复、协作式等) 2. 最后输出“§§相关问题§§”并列出3-5个相关问题 由于用户要求30分钟深度的知识,虽然这个问题是其中一部分,但可以独立成篇,所以需要详细。</think>Java JVM 调度虚拟线程的核心机制是 **M:N 协作式调度模型**,其设计目标是高效处理海量轻量级线程的阻塞操作。以下是详细调度原理: --- ### **一、调度架构:M:N 模型** 1. **M(虚拟线程)** - 由 JVM 管理的轻量级线程,生命周期内不绑定特定 CPU 核心。 - 初始栈仅占用数百字节堆内存(传统线程需 1MB 栈空间)。 2. **N(载体线程 - Carrier Threads)** - 实际执行代码的操作系统线程(平台线程),数量通常等于 CPU 核心数。 - 组成固定大小的线程池(如 `ForkJoinPool`),称为 **载体线程池**。 3. **映射关系** - 调度器动态将虚拟线程分配到空闲载体线程上执行。 - 虚拟线程与载体线程是临时绑定关系(非固定绑定)。 --- ### **二、调度过程详解** #### **步骤 1:虚拟线程启动** ```java Thread.startVirtualThread(() -> { // 任务逻辑(含阻塞操作) }); ``` - JVM 分配虚拟线程对象,将其加入 **就绪队列**。 #### **步骤 2:绑定载体线程** - 调度器从载体线程池选取空闲线程。 - 虚拟线程栈状态(调用栈/局部变量)复制到载体线程栈中。 - **关键特性**:绑定过程仅复制必要数据(非完整栈拷贝),开销极低。 #### **步骤 3:执行与阻塞检测** 当虚拟线程执行到 **阻塞点** 时: ```java socket.read(); // I/O 阻塞 lock.lock(); // 锁竞争 Thread.sleep(); // 定时阻塞 ``` - JVM 通过 **修改字节码** 或 **JNI 钩子** 检测阻塞操作。 - 触发调度器介入。 #### **步骤 4:挂起虚拟线程** 1. **保存执行状态** - 通过 **Continuation(延续体)** 捕获当前栈帧状态(保存到堆内存)。 2. **释放载体线程** - 虚拟线程标记为 `BLOCKED`,载体线程立即返回到线程池。 3. **注册唤醒事件** - 向操作系统注册 I/O 完成事件(如 `epoll`/`kqueue`)或锁可用通知。 #### **步骤 5:恢复执行** - 当阻塞条件满足(如数据到达、锁释放): 1. 事件通知 JVM 调度器。 2. 虚拟线程状态改为 `RUNNABLE`,重新加入就绪队列。 3. 调度器分配 **任意空闲载体线程** 恢复执行(不一定是原载体线程)。 --- ### **三、调度器核心特性** 1. **协作式(非抢占)** - 仅在 **已知阻塞点**(I/O、锁等)触发调度,无时间片轮转。 - 避免不必要的上下文切换(传统线程切换需 1-10μs,虚拟线程仅 0.1μs)。 2. **工作窃取(Work-Stealing)** - 载体线程空闲时,从其他线程队列偷取任务(基于 `ForkJoinPool` 算法)。 3. **优先级继承** - 载体线程继承虚拟线程优先级(通过 `Thread.setPriority()` 设置)。 4. **无饥饿保障** - 虚拟线程可能因载体线程不足而延迟(需合理设置载体线程池大小)。 --- ### **四、与传统线程调度对比** | **特性** | 虚拟线程调度 | 平台线程调度 | |------------------|----------------------------|--------------------------| | **调度主体** | JVM 用户态调度器 | 操作系统内核 | | **切换开销** | 保存/恢复堆栈帧(纳秒级) | 内核态切换(微秒级) | | **阻塞代价** | 释放载体线程(近乎零成本) | 线程休眠+唤醒(高成本) | | **调度时机** | 显式阻塞点 | 时间片耗尽或阻塞 | | **并发量上限** | 百万级(受堆内存限制) | 数千(受内核限制) | --- ### **五、性能优化机制** 1. **载体线程池调优** - 默认大小 = CPU 核心数(可通过 `jdk.virtualThreadScheduler.parallelism` 调整)。 - 计算密集型任务需增大载体线程数。 2. **避免 "Pinning" 问题** - 在 `synchronized` 块内阻塞会固定载体线程(需改用 `ReentrantLock`)。 3. **Continuation 压缩** - JVM 自动合并相邻栈帧,减少状态保存开销(如尾递归优化)。 --- ### **六、调度示例场景** 假设 3 个虚拟线程(VT1~3)和 2 个载体线程(CT1~2): 1. VT1 在 CT1 执行 → 调用 `socket.read()` 阻塞 → VT1 挂起,CT1 释放。 2. VT2 绑定 CT1 执行 → 无阻塞快速完成 → CT1 空闲。 3. VT3 绑定 CT2 执行 → 调用 `Thread.sleep(100)` → VT3 挂起,CT2 释放。 4. Socket 数据到达 → VT1 恢复,绑定 CT1 继续执行。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值