虚拟线程浅析

在传统Java线程模型中,每个线程都要占用操作系统的线程资源。创建与销毁线程都会消耗时间和系统资源,线程上下文切换也带来了额外的成本。

当需要处理大量并发任务时,线程数量急剧上升,导致系统资源的浪费和整体性能的下降。

线程池通过复用已有线程,减少了频繁的创建与销毁,从而提升程序的性能和响应速度。

不过,任务的IO操作和并发锁常常引发线程的等待和阻塞,进而产生上下文切换,造成CPU资源的浪费,影响应用性能。尤其是在IO密集型服务中,CPU与IO的速度不匹配,使得CPU资源遭遇更大浪费。

什么是虚拟线程?它和平台线程、内核线程的区别是什么?

虚拟线程是 JDK 21+ 正式提供的轻量级用户态线程,由 JVM 调度,不直接绑定 OS 线程。

◦ 内核线程:OS 管理,重量级,数量有限。

◦ 平台线程:绑定内核线程,Java 传统线程。每个线程对应操作系统的真实线程,创建成本高,占用内存大

◦ 虚拟线程:依附平台线程。很小、动态扩容,不绑定 OS 线程,调度在用户态,资源消耗极低,所以能轻松创建百万

• 传统平台线程:线程一阻塞,内核线程直接被占死,全程不释放,干等着。

• 虚拟线程:用户态阻塞感知,只要虚拟线程一阻塞,载体平台线程马上脱身复用,没有空闲浪费。

虚拟线程是否共享栈?是否共享程序计数器?

不共享。每个虚拟线程有独立栈、独立程序计数器,只是很小、很轻。

虚拟线程的调度模型是什么?谁来调度?

虚拟线程采用 M:N 调度:M 个虚拟线程映射到 N 个平台线程。

由 JVM 的 ForkJoinPool 调度器 调度。

虚拟线程的核心设计目标是什么?解决了什么问题?

目标:高并发、低内存、代码风格不变。

解决:传统线程太重、高并发下内存占用大、IO密集任务下,CPU利用率低

核心组件

虚拟线程(VirtualThread)

JVM层面实现,java.lang.Thread子类,不直接绑定OS线程。

极轻量:默认栈几百字节、动态扩容,创建/销毁几乎零成本。

自身无执行能力,必须挂载到载体线程才能运行。

载体线程(Carrier Thread)

即传统平台线程,1:1对应OS线程,是虚拟线程的执行载体。

默认数量=CPU核心数(ForkJoinPool),同一时间只跑一个虚拟线程。

调度器(Scheduler)

默认:ForkJoinPool(优化版),管理就绪虚拟线程队列。

负责虚拟线程的挂载(mount)、卸载(unmount)、调度复用。

Continuation(续体)

本质:执行状态快照,保存程序计数器、局部变量、操作数栈、调用栈。

作用:让虚拟线程可暂停/恢复,像游戏存档/读档。

虚拟线程栈:堆上分段栈,挂起时拷贝到堆,恢复时再加载

完整运行流程

提交:虚拟线程start(),进入就绪队列。

挂载:调度器分配空闲载体线程,虚拟线程开始执行。

阻塞检测:遇到IO、sleep、Lock等阻塞点,JVM识别并触发卸载。

保存续体:将当前执行状态封装为Continuation存入堆内存。

卸载:虚拟线程从载体线程解绑,载体线程立即释放。

载体复用:载体线程从队列取下一个虚拟线程执行。

等待完成:底层IO以非阻塞方式进行,JVM注册回调。

重新挂载:IO完成后,虚拟线程重回队列;载体空闲时,恢复续体、继续执行。

虚拟线程适合什么场景?不适合什么场景

• 适合:IO 密集型(HTTP、DB、RPC、文件等)。

• 不适合:CPU 密集型,和传统线程无优势。

虚拟线程能不能替代线程池?为什么?

虚拟线程不需要池化,每次直接 new 即可。

高并发 IO 场景下,可以替代传统线程池。

使用虚拟线程时,为什么不建议使用线程局部变量(ThreadLocal)?

虚拟线程大量创建,ThreadLocal 会导致内存占用飙升,且生命周期难管理。建议用 ScopedValue。

为什么官方不建议对虚拟线程使用池化?

虚拟线程本来就是用完即丢、极轻量,池化反而增加复杂度,没有意义。

如何创建、启动、运行虚拟线程?写出两种方式。

// 方式1

Thread.startVirtualThread(() -> {});

// 方式2

Thread vt = Thread.ofVirtual().unstarted(() -> {});

vt.start();

Synchronized 会不会导致 pinning?为什么?

会。虚拟线程进入 synchronized 后会被钉住,阻塞时无法卸载,平台线程被占用。

monitor 实现把"锁的拥有者"记成了载体线程(OS 线程),而不是虚拟线程本身。为了保证 synchronized 的互斥语义不被破坏,JVM 只能禁止虚拟线程在 synchronized 块内卸载(unmount),于是载体线程被一起拖住。

如果允许虚拟线程在 synchronized 块里阻塞并 unmount:

锁 owner 记录的是 carrier A

虚拟线程 V1 卸下,carrier A 被调度去跑虚拟线程 V2

V2 因为跑在 carrier A 上,JVM 会认为"carrier A 持有这个 monitor"

V2 就能进同一个对象的 synchronized 方法 / 调用 monitorexit 释放锁

虚拟线程执行阻塞 IO 时,底层是怎么做到不阻塞平台线程的?

JVM 对 NIO 做了虚拟线程感知:

阻塞时 → 保存线程现场 → 卸载虚拟线程 → 平台线程去做别的 → IO 完成后重新挂载运行。

虚拟线程抛出异常时,栈信息有什么特点?

虚拟线程栈会包含挂载/卸载的完整调用链,dump 时能看到完整逻辑栈,方便排查。

虚拟线程的Continuation是什么作用?

Continuation 是虚拟线程的核心组件,负责保存/恢复执行现场,实现挂起和恢复。

虚拟线程是用户态调度还是内核态调度?

用户态调度,不陷入内核,开销极低。

为什么虚拟线程能极大提高 IO 密集型服务的吞吐量?

线程极轻量,阻塞时能快速切换,CPU 几乎不会空闲,连接数、吞吐量大幅提升。

生产环境直接把所有平台线程换成虚拟线程,可能会遇到什么问题?

◦ synchronized 导致 pinning,性能反而下降

◦ ThreadLocal 内存暴涨

◦ 依赖线程池、线程数量的代码逻辑异常

你线上用虚拟线程遇到过什么坑?怎么排查的?

遇到过 synchronized 导致 pinning,并发上不去。

◦ 改成 ReentrantLock 解决,因为 ReentrantLock 不会钉住虚拟线程。

什么情况下虚拟线程不但不提升性能,反而更慢?

◦ 大量 synchronized / JNI

◦ CPU 密集

◦ 深度递归、大栈帧

你怎么监控、观测、Dump 虚拟线程?

jstack、jcmd 可以看到虚拟线程

JFR 监控虚拟线程的挂载、卸载、pinning 事件

相关推荐
摇滚侠1 小时前
《SpringBoot 3:入门与应用实战》第 7 章 AOP 思想与实现 阅读笔记 13
java·spring boot·笔记
Mr. zhihao1 小时前
线上卡顿排查决策手册:场景 × 命令 × 现象 × 调整方案
java·开发语言·jvm
FYKJ_20101 小时前
springboot网上购书商城---附源码15749
java·spring boot·后端·python·spark·django·php
LayZhangStrive1 小时前
Java IDEA - 各种快捷键设置汇总(提高工具使用效率)
java·idea·设置·快捷键
snow@li2 小时前
Java:事务管理全景梳理/解决业务系统数据一致性问题
java
额额额对了2 小时前
Linux 进程管理详解:从概念到实战
java·服务器·前端
evans在进步2 小时前
Spring Boot 启动流程与 @SpringBootApplication 核心原理详解
java·spring boot·后端
许彰午2 小时前
11-rawSql占位符替换
java·低代码·架构
码匠许师傅2 小时前
【C++ 面试真题】聊聊 C++ 的类型特征( type_traits)
java·c++·面试