JDK21之虚拟线程的深入理解
目录
1、 虚拟线程
1.1、线程术语定义
1.2、虚拟线程创建
1.3、设计目标
1.4、核心思想
1.5、与传统线程的区别
2、工作原理
2.1、介绍
2.2、组成
2.3、工作过程
3、提高吞吐量的演变
3.1、串行模式
3.2、线程池+Future异步调用
3.3、线程池+CompletableFuture异步调用
4、虚拟线程的特点和优势
4.1、优势:
4.2、注意限制
4.3、适用场景
前言
JDK21于9月19日发布,虚拟线程是亮点之一。它改变高吞吐代码编写方式,提升IO密集型程序吞吐量。
这个 虚拟线程(Virtual Thread) 是 Java 近两年最重磅的特性之一(Project Loom 项目),很多资深 Java 程序员都在等它,它直接冲击了 Java 长久以来的并发编程模型。
如下所示:
经典类比:
以前 Java 的线程就像一辆辆大巴车(重量级,启动成本高);
现在的虚拟线程更像很多小电动车(数量多,快速启停,占用内存低)。
接下来让我们一起看两者的区别和联系!
1、 虚拟线程
1.1、线程术语定义
如下所示:
由操作系统管理,是操作系统调度的基本单位。
2、平台线程(
Platform Thread)
:
Java.Lang.Thread 类的每个实例,都是一个平台线程,是 Java 对操作系统线程的包装,与操作系统是 1:1 映射。
3、虚拟线程(
Virtual Thread)
:
一种轻量级,由 JVM 管理的线程。对应的实例 java.lang.VirtualThread 这个类。它在平台线程上运行 Java 代码,但在代码的整个生命周期内不独占平台线程。
4、载体线程(
Carrier Thread)
:
指真正负责执行虚拟线程中任务的平台线程。一个虚拟线程装载到一个平台线程之后,那么这个平台线程就被称为虚拟线程的载体线程。
1.2、虚拟线程创建
1:直接创建虚拟线程
public static void main(String[] args) throws InterruptedException {Thread vt = Thread.startVirtualThread(() -> {try {Thread.sleep(500); // 模拟任务耗时} catch (InterruptedException e) {e.printStackTrace();}System.out.println("hello world virtual thread");});System.out.println("主线程等待虚拟线程...");vt.join(); // 阻塞等虚拟线程完成System.out.println("主线程执行结束");}输出
主线程等待虚拟线程...
hello world virtual thread
主线程执行结束
2:创建虚拟线程但不自动运行,手动调用start()开始运行
public class ThreadTest {public static void main(String[] args) throws InterruptedException {Thread virtualThread = Thread.ofVirtual().unstarted(() -> {try {Thread.sleep(5000); // 模拟任务耗时} catch (InterruptedException e) {e.printStackTrace();}System.out.println("hello world virtual thread");});virtualThread.start();virtualThread.join(); // 等虚拟线程运行完再退出主线程System.out.println("主线程结束");}
}输出:
hello world virtual thread
主线程执行结束
如果你不想手动join() 线程,可以让虚拟线程跑在一个受管理的线程池中,这样线程池的 close() 会自动等待任务完成:
try (var executor =Executors.newVirtualThreadPerTaskExecutor()){executor.submit(() -> {try {Thread.sleep(3000); // 模拟任务耗时} catch (InterruptedException e) {e.printStackTrace();}System.out.println("hello world virtual thread");});}输出:
hello world virtual thread
3:通过虚拟线程的 ThreadFactory 创建虚拟线程
ThreadFactory threadFactory = Thread.ofVirtual().factory();Thread thread = threadFactory.newThread(() -> {try {Thread.sleep(1000); // 模拟任务耗时} catch (InterruptedException e) {e.printStackTrace();}System.out.println("hello world virtual thread");});thread.start();thread.join();输出:
hello world virtual thread
4:Executors.newVirtualThreadPer
-TaskExecutor():
不论是平台线程还是虚拟线程,只要它是异步启动的,如果 main 线程不等待它执行完,JVM 是可以提前结束的;
因此可以让虚拟线程跑在一个受管理的线程池中,这样线程池的 close() 会自动等待任务完成:
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
executor.submit(() -> {System.out.println("Start virtual thread...");Thread.sleep(1000);System.out.println("End virtual thread.");return true;
});
1.3、设计目标
大幅提升 Java 在高并发场景的可扩展性,让你用原生线程的编程模型,写出能支撑成千上万甚至百万并发的程序。
为什么虚拟线程能支持百万并发?
- 调度在用户态完成(JVM 控制),切换虚拟线程时不需要内核态上下文切换 → 极快。
- 虚拟线程的栈是在堆里分配的,并且是按需增长的,不像 OS 线程一启动就分配几百 KB ~ MB 固定栈空间。
- 新建、销毁成本极低,你可以"像创建对象一样"创建线程。
1.4、核心思想
如下所示:
线程调度从 OS 内核转移到 JVM 用户态,线程由 JVM 自己管理和调度(轻量级)。
1.5、与传统线程的区别
如下所示:
2、工作原理
2.1、介绍
虚拟线程是由 Java 虚拟机调度,而不是操作系统。虚拟线程占用空间小,同时使用轻量级的任务队列来调度虚拟线程,避免了线程间基于内核的上下文切换开销,因此可以极大量地创建和使用。
2.2、组成
如下所示:
简单来看,虚拟线程实现如下:
virtual thread =continuation+scheduler+runnable
2.3、工作过程
虚拟线程会把任务(java.lang.Runnable实例)包装到一个 Continuation
实例中:
-
当任务需要阻塞挂起的时候,会调用 Continuation 的
yield
操作进行阻塞,虚拟线程会从平台线程卸载。 -
当任务解除阻塞继续执行的时候,调用
Continuation.run
会从阻塞点继续执行。
Scheduler
也就是执行器,由它将任务提交到具体的载体线程池中执行。
-
它是 java.util.concurrent.Executor 的子类。
-
虚拟线程框架提供了一个默认的 FIFO 的 ForkJoinPool 用于执行虚拟线程任务。
Runnable
则是真正的任务包装器,由 Scheduler 负责提交到载体线程池中执行。
JVM 把虚拟线程分配给平台线程的操作称为 mount(挂载)
,取消分配平台线程的操作称为 unmount(卸载
)
:
mount 操作
:虚拟线程挂载到平台线程,虚拟线程中包装的 Continuation 堆栈帧数据会被拷贝到平台线程的线程栈,这是一个从堆复制到栈的过程。
unmount 操作
:虚拟线程从平台线程卸载,此时虚拟线程的任务还没有执行完成,所以虚拟线程中包装的 Continuation 栈数据帧会会留在堆内存中。
从 Java 代码的角度来看,其实是看不到虚拟线程及载体线程共享操作系统线程的,会认为虚拟线程及其载体都在同一个线程上运行,因此,在同一虚拟线程上多次调用的代码可能会在每次调用时挂载的载体线程都不一样。JDK 中使用了 FIFO 模式的 ForkJoinPool 作为虚拟线程的调度器,从这个调度器看虚拟线程任务的执行流程大致如下:
-
调度器(线程池)中的平台线程等待处理任务。
-
一个虚拟线程被分配平台线程,该平台线程作为载体线程执行虚拟线程中的任务。
-
虚拟线程运行其 Continuation,Mount(挂载)平台线程后,最终执行 Runnable 包装的用户实际任务。
-
虚拟线程任务执行完成,标记 Continuation 终结,标记虚拟线程为终结状态,清空上下文,等待 GC 回收,解除挂载载体线程会返还到调度器(线程池)中等待处理下一个任务。
3、提高吞吐量的演变
为了提升吞吐性能,我们所做的优化
1. 串行模式
2. 线程池 +Future 异步调用
3. 线程池 +CompletableFuture 异步调用
3.1、串行模式
如下所示:
在当前的微服务架构下,处理一次用户/上游的请求,往往需要多次调用下游服务、数据库、文件系统等,再将所有请求的数据进行处理最终的结果返回给上游。
在这种模式下,使用串行模式去查询数据库,下游 Dubbo/Http 接口,文件系统完成一次请求,接口整体的耗时等于各个下游的返回时间之和。
这种写法虽然简单,但是接口耗时长、性能差,无法满足 C 端高 QPS 场景下的性能要求。
3.2、线程池+Future异步调用
为了解决串行调用的低性能问题,会考虑使用并行异步调用的方式,最简单的方式便是使用线程池 +Future 去并行调用。
代码示例如下:
这种方式虽然解决了大部分场景下的串行调用低性能问题,但是也存在着严重的弊端,由于存在 Future 的前后依赖关系,当使用场景存在大量的前后依赖时,会使得线程资源和 CPU 大量浪费在阻塞等待上,导致资源利用率低。
3.3、线程池+CompletableFuture异步调用
为了降低 CPU 的阻塞等待时间和提升资源的利用率,我们会使用
CompletableFuture
对调用流程进行编排,降低依赖之间的阻塞。
使用 CompletableFuture 的实现方式如下:
CompletableFuture 虽然一定程度上面缓解了 CPU 资源大量浪费在阻塞等待上的问题,但是只是缓解,核心的问题始终没有解决。这两个问题导致 CPU 无法充分被利用,系统吞吐量容易达到瓶颈。
-
线程资源浪费瓶颈始终在 IO 等待上
,导致 CPU 资源利用率较低。目前大部分服务是 IO 密集型服务,一次请求的处理耗时大部分都消耗在等待下游 RPC,数据库查询的 IO 等待中,此时线程仍然只能阻塞等待结果返回,导致 CPU 的利用率很低。 -
线程数量存在限制, 为了增加并发度,我们会给线程池配置更大的线程数,但是线程的数量是有限制的,Java 的线程模型是 1:1 映射平台线程的,导致 Java 线程创建的成本很高,不能无限增加。同时随着 CPU 调度线程数的增加,会导致更严重的资源争用,宝贵的 CPU 资源被损耗在上下文切换上。
那么有没有一种方法可以易于编写,方便迁移,符合日常编码习惯,同时性能很不错,CPU 资源利用率较高的方案呢?
JDK21 中的虚拟线程可能给出了答案
, JDK 提供了与 Thread 完全一致的抽象 Virtual Thread
来应对这种经常阻塞的情况,阻塞仍然是会阻塞,但是换了阻塞的对象,由昂贵的平台线程阻塞改为了成本很低的虚拟线程的阻塞,当代码调用到阻塞 API 例如 IO,同步,Sleep 等操作时,JVM 会自动把 Virtual Thread 从平台线程上卸载
,平台线程就会去处理下一个虚拟线程,通过这种方式,提升了平台线程的利用率,让平台线程不再阻塞在等待上,从底层实现了少量平台线程就可以处理大量请求,提高了服务吞吐和 CPU 的利用率
。
4、虚拟线程的特点和优势
4.1、优势:
- 超高并发能力
轻松创建百万线程,不受 OS 线程数限制。 - 阻塞友好
你不用为了性能写异步回调代码(CompletableFuture那类),可以直接用同步阻塞方式,虚拟线程会帮你优化。 - 完全兼容现有 API
任何用 Thread 的地方都能用虚拟线程替换。 - 调试方便
栈追踪、调试和平台线程一样自然。
4.2、注意限制
- 虚拟线程 ≠ 全异步
只是阻塞不会浪费 OS 资源,但计算密集型任务依然受限于 CPU 核心数。 - 某些 JNI 调用、本地 IO、旧版本依赖库 可能阻塞宿主 OS 线程(Carrier Thread),会影响并发度。
- 不建议用 ThreadLocal 保存大量数据(虚拟线程复用会产生额外内存消耗)。
4.3、适用场景
- 高并发后端服务(百万级连接的 WebSocket 服务、网关)
- 传统阻塞式 IO 模型(数据库、文件、HTTP 调用)
- 网络爬虫、批量任务(以前得用线程池限制,现在可直接虚拟线程跑)
- 替代 Reactor/Netty 异步编程(同步代码性能也高)
不适合:
- 单核 CPU 密集计算(虚拟线程不能增加 CPU 核心)
- 无法释放 OS 线程的长阻塞任务(比如 JNI 调用)
参考文章
1、虚拟线程原理及性能分析-CSDN博客文章浏览阅读610次,点赞4次,收藏7次。JDK21于9月19日发布,虚拟线程是亮点之一。它改变高吞吐代码编写方式,提升IO密集型程序吞吐量。本文介绍为提升吞吐做的优化,如线程池异步调用;对比一请求一线程模型;阐述虚拟线程定义、原理、内存占用等,还进行压测,显示其性能佳且降低开发难度。https://blog.csdn.net/gaoliang1719/article/details/134760246?ops_request_misc=%257B%2522request%255Fid%2522%253A%25227a0f3e3e9ecb749df1d7fbe8ac0966c5%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=7a0f3e3e9ecb749df1d7fbe8ac0966c5&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-134760246-null-null.142^v102^control&utm_term=%E8%99%9A%E6%8B%9F%E7%BA%BF%E7%A8%8B&spm=1018.2226.3001.4187