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

JDK21 虚拟线程原理剖析与性能深度解析

一、背景

JDK21 于 2023 年 9 月 19 日正式发布,带来众多新特性,其中虚拟线程(Virtual Thread)无疑是最受关注的亮点。它的出现,彻底革新了高吞吐量代码的编写范式。

在 IO 密集型程序开发中,仅需对现有代码进行少量修改,就能显著提升程序吞吐量,让我们能轻松实现高并发性能。

二、提升吞吐性能的优化演进

在深入了解虚拟线程之前,我们先来回顾在追求高吞吐性能过程中,常见的优化方案及发展历程。

(一)串行模式

在当下的微服务架构中,处理一次用户或上游请求,往往需要多次调用下游服务、数据库、文件系统等资源,并对获取的数据进行整合处理后,才能将最终结果返回给上游。

采用串行模式查询数据库、调用下游接口或访问文件系统时,完成一次请求的总耗时等于各个下游操作的返回时间之和。这种方式虽然代码逻辑简单,但存在接口响应时间长、性能低下的问题,无法满足 C 端高 QPS 场景下的性能需求。

(二)线程池 + Future 异步调用

为解决串行调用的性能瓶颈,并行异步调用成为首选方案,其中最基础的实现方式便是使用线程池结合 Future 进行并行调用。

通过线程池管理线程资源,利用 Future 获取异步计算结果。然而,这种方式存在明显弊端。当业务场景中存在大量具有前后依赖关系的操作时,线程资源和 CPU 会大量浪费在阻塞等待 Future 结果上,导致资源利用率大幅降低。

(三)线程池 + CompletableFuture 异步调用

为进一步降低 CPU 在阻塞等待上的时间消耗,提升资源利用率,Java 8 引入的 CompletableFuture 被广泛应用于调用流程编排。

相较于原生 Future,CompletableFuture 原生支持通过设置回调方法处理计算结果,并且提供了强大的组合编排操作,有效解决了回调地狱问题,使代码更具可读性。但 CompletableFuture 并未从根本上解决两个核心问题:

  1. 线程资源浪费:在 IO 密集型服务中,大量线程因等待下游 RPC 调用、数据库查询等 IO 操作而阻塞,导致 CPU 资源利用率低下。

  2. 线程数量限制:Java 线程模型采用 1:1 映射平台线程,创建成本高昂,无法无限扩展。随着 CPU 调度线程数增加,上下文切换开销增大,宝贵的 CPU 资源被大量消耗在无意义的切换操作中。

三、解决一请求一线程模型的困境

在 Web 应用开发中,一请求一线程模型是最常见的请求处理方式,即每个请求都由单独的线程负责处理。该模型具有易于理解、编码可读性高、调试方便等优点,但也存在明显缺陷。

当线程执行如数据库连接、网络调用等阻塞操作时,线程会被阻塞,直至操作完成,在此期间无法处理其他请求。在大促或突发流量场景下,为保证请求快速响应,有什么?通常会采用以下解决方案:

  1. 扩大服务最大线程数:该方法简单直接,但受限于系统资源,平台线程数量无法无限制扩充。过多的平台线程会消耗大量系统资源用于上下文切换,且每个平台线程默认开辟约 1MB 的私有栈空间,大量线程会占用巨额内存。

  2. 垂直扩展与水平扩展:通过升级机器配置(垂直扩展)或增加服务节点(水平扩展)提升服务性能,这是最常见的解决方案,但会显著增加成本,且在某些场景下扩容也无法完全解决性能瓶颈问题。

  3. 异步 / 响应式编程方案:采用 RPC NIO 异步调用、WebFlux、Rx-Java 等基于事件驱动的非阻塞框架,可通过少量线程实现高吞吐量请求处理,资源利用率高。但此类方案学习成本高,与传统一请求一线程模型编码风格差异大,代码调试困难,且存在兼容性问题。

JDK21 引入的虚拟线程为我们提供了全新的解决方案。它通过低成本的虚拟线程替代昂贵的平台线程进行阻塞操作,当代码执行到如 IO 操作、同步操作、Sleep 等阻塞 API 时,JVM 会自动将虚拟线程从平台线程上卸载,使平台线程能够继续处理其他虚拟线程任务,从而在底层实现用少量平台线程处理大量请求,大幅提升服务吞吐量和 CPU 利用率。

四、虚拟线程技术详解

(一)线程术语定义

  1. 操作系统线程(OS Thread):由操作系统直接管理,是操作系统进行任务调度的基本单位。
  2. 平台线程(Platform Thread):Java 中java.lang.Thread类的每个实例对应一个平台线程,它是对操作系统线程的封装,与操作系统线程呈 1:1 映射关系。
  3. 虚拟线程(Virtual Thread):一种轻量级线程,由 JVM 负责管理,对应的实例为java.lang.VirtualThread类。
  4. 载体线程(Carrier Thread):实际执行虚拟线程中任务的平台线程,当虚拟线程装载到某个平台线程上时,该平台线程即成为其载体线程。

(二)虚拟线程定义

平台线程在底层操作系统线程上运行 Java 代码,且在代码生命周期内独占操作系统线程,其数量受限于操作系统线程数量,由系统内核线程调度程序进行调度。

而虚拟线程不与特定操作系统线程绑定,它在平台线程上运行 Java 代码,但不会在整个生命周期内独占平台线程。多个虚拟线程可共享同一个平台线程,且虚拟线程创建成本极低,其数量可远超平台线程数量。

(三)虚拟线程创建

  1. 直接创建并自动运行
Thread vt = Thread.startVirtualThread(() -> {System.out.println("hello wolrd virtual thread");
});
  1. 手动控制运行
Thread vt = Thread.ofVirtual().unstarted(() -> {System.out.println("hello wolrd virtual thread");
});
vt.start();
  1. 通过 ThreadFactory 创建
ThreadFactory tf = Thread.ofVirtual().factory();
Thread vt = tf.newThread(() -> {System.out.println("Start virtual thread...");Thread.sleep(1000);System.out.println("End virtual thread. ");
});
vt.start();
  1. 使用 Executors 创建
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
executor.submit(() -> {System.out.println("Start virtual thread...");Thread.sleep(1000);System.out.println("End virtual thread.");return true;
});

(四)虚拟线程实现原理

虚拟线程由 Java 虚拟机调度,具有占用空间小的特点,通过轻量级任务队列进行调度,避免了基于内核的线程上下文切换开销,因此可大量创建和使用。从实现角度看,虚拟线程可表示为virtual thread = continuation + scheduler + runnable

  • Continuation:负责包装任务(java.lang.Runnable实例)。当任务需要阻塞挂起时,调用Continuationyield操作,虚拟线程从平台线程卸载;任务解除阻塞继续执行时,调用Continuation.run从阻塞点恢复执行。

  • Scheduler:作为执行器,继承自java.util.concurrent.Executor,负责将任务提交到具体的载体线程池中执行。虚拟线程框架默认提供一个 FIFO(先进先出)的 ForkJoinPool 用于执行虚拟线程任务。

  • Runnable:实际的任务包装器,由 Scheduler 提交到载体线程池中执行。

JVM 将虚拟线程分配给平台线程的过程称为mount(挂载),取消分配称为unmount(卸载)。在没有阻塞的场景下,虚拟线程任务执行流程如下:

  1. 调度器(线程池)中的平台线程等待处理任务。
  2. 虚拟线程被分配到平台线程,该平台线程作为载体线程执行虚拟线程任务。
  3. 虚拟线程运行其Continuation,挂载到平台线程后,执行Runnable包装的用户实际任务。
  4. 任务执行完成,标记Continuation终结,虚拟线程进入终结状态,清空上下文,等待 GC 回收,载体线程返回调度器等待处理下一个任务。

当遇到阻塞场景(如获取锁操作)时,以获取ReentrantLock锁为例:

ReentrantLock lock = new ReentrantLock();
Thread.startVirtualThread(() -> {lock.lock();
});
// 确保锁已被持有
Thread.sleep(1000);
Thread.startVirtualThread(() -> {System.out.println("first");// 触发Continuation的yield操作lock.lock();try {System.out.println("second");} finally {lock.unlock();}System.out.println("third");
});
Thread.sleep(Long.MAX_VALUE);

此时,虚拟线程执行任务时调用Continuation#run(),在尝试获取锁时触发阻塞操作,导致Continuationyield操作让出控制权。若yield操作成功,虚拟线程从载体线程unmount,载体线程栈数据移动到Continuation栈数据帧并保存在堆内存中,载体线程返回执行器等待新任务;若yield操作失败,则对载体线程进行Park调用,虚拟线程和载体线程同时阻塞(本地方法、Synchronized修饰的同步方法等会导致yield失败)。

当锁持有者释放锁后,唤醒虚拟线程重新获取锁。获取锁成功后,虚拟线程重新mount,可能分配到另一个载体线程执行,Continuation栈数据帧恢复到载体线程栈,再次调用Continuation#run()恢复任务执行。任务完成后,虚拟线程和载体线程按正常流程处理。

通过以下代码可直观感受Continuation的暂停和恢复功能(需添加编译参数--add-exports java.base/jdk.internal.vm=ALL-UNNAMED):

ContinuationScope scope = new ContinuationScope("scope");
Continuation continuation = new Continuation(scope, () -> {System.out.println("before yield开始");Continuation.yield(scope);System.out.println("after yield 结束");
});
System.out.println("1 run");
// 第一次执行Continuation.run
continuation.run();
System.out.println("2 run");
// 第二次执行Continuation.run
continuation.run();
System.out.println("Done");

执行结果显示,Continuation实例调用yield后,再次调用run方法可从yield处继续执行,实现程序的中断和恢复。

(五)虚拟线程内存占用评估

线程类型 资源占用详情
平台线程 1. 预留 1MB 线程栈空间 2. 线程实例占用 2000+ byte 数据
虚拟线程 1. Continuation栈占用数百 byte 到数百 KB 内存,存储在 Java 堆中 2. 线程实例占用 200 - 240 byte 数据

通过实际测试程序验证:

// 测试平台线程内存占用
private static final int COUNT = 4000;
public static void main(String[] args) throws Exception {for (int i = 0; i < COUNT; i++) {new Thread(() -> {try {Thread.sleep(Long.MAX_VALUE);} catch (Exception e) {e.printStackTrace();}}, String.valueOf(i)).start();}Thread.sleep(Long.MAX_VALUE);
}

运行后,通过-XX:NativeMemoryTracking=detail参数和JCMD命令查看,4000 个左右的平台线程的线程栈空间占用约为使用内存的 96% 以上。

// 测试虚拟线程内存占用
private static final int COUNT = 4000;
public static void main(String[] args) throws Exception {for (int i = 0; i < COUNT; i++) {Thread.startVirtualThread(() -> {try {Thread.sleep(Long.MAX_VALUE);} catch (Exception e) {e.printStackTrace();}});}Thread.sleep(Long.MAX_VALUE);
}

运行 4000 个虚拟线程后,堆内存和总内存实际占用均不超过 300MB,证明虚拟线程在大量创建时不会占用过多内存,且其堆栈可被 GC 回收,进一步降低内存占用。

(六)虚拟线程的局限及使用建议

  1. 调用native方法或外部方法(Foreign Function & Memory API,jep 424 )时,虚拟线程无法进行yield操作,会导致载体线程阻塞。

  2. synchronized修饰的代码块或方法中,虚拟线程不能进行yield操作,建议使用ReentrantLock替代。

  3. 虽然虚拟线程支持ThreadLocal,但由于虚拟线程数量庞大,会导致ThreadLocal中存储的线程变量过多,频繁触发 GC,影响性能。官方建议尽量少用ThreadLocal,避免在其中存储大对象,未来计划通过ScopedLocal替代ThreadLocal,但 JDK21 尚未正式发布该功能。

  4. 虚拟线程资源占用极少,可大量创建,无需进行池化操作。池化反而会引入额外开销,应采用即用即创建、用完即销毁的方式。

(七)虚拟线程适用场景

  1. 大量存在 IO 阻塞等待的任务,如下游 RPC 调用、数据库查询等场景。

  2. 批量处理执行时间较短的计算任务。

  3. 基于Thread-per-request(一请求一线程)风格的应用程序,如主流的 Tomcat 线程模型、基于该模型实现的 SpringMVC 框架等,只需少量改动即可大幅提升应用吞吐量。

五、虚拟线程压测性能分析

为直观展现虚拟线程的性能优势,我们模拟 Web 容器处理 Http 请求的常见场景,进行性能压测对比。

(一)测试场景

  1. 场景一:在 Spring Boot 中使用内嵌 Tomcat 处理 Http 请求,采用默认平台线程池作为请求处理线程池。

  2. 场景二:使用 Spring-WebFlux 创建基于事件循环模型的应用程序,实现响应式请求处理。

  3. 场景三:在 Spring Boot 中使用内嵌 Tomcat 处理 Http 请求,将 Tomcat 的请求处理线程池替换为虚拟线程池(Tomcat 10 版本已支持虚拟线程)。

(二)测试流程

  1. 使用 Jmeter 开启 500 个线程并行发起请求,每个线程等待请求响应后再发起下一次请求,单次请求超时时间设为 10s,测试持续 60s。

  2. 测试的 Web Server 接收 Jmeter 请求,调用慢速服务器获取响应并返回。

  3. 慢速服务器以随机超时响应,最大响应时间 1000ms,平均响应时间 500ms。

(三)衡量指标

主要关注吞吐量(单位时间内处理的请求数量)和平均响应时间,吞吐量越高、平均响应时间越低,代表性能越好。

(四)测试结果与分析

  1. Tomcat + 普通线程池
  • 默认线程池:Tomcat 默认采用一请求一线程模型,线程池最多包含 200 个线程。由于每个请求会阻塞调用平均 RT 为 500ms 的慢速服务器,预期吞吐量为每秒 400 个请求,实际压测结果为 388 req/sec,接近预期值。

  • 增加线程池:在生产环境中,为提升吞吐量,通常会增大线程池大小(如设置server.tomcat.threads.max=500+)。调整后,吞吐量随线程数量增加呈比例上升,平均 RT 趋近于慢速服务器的平均响应时间。但需注意,平台线程受内存和 Java 线程映射模型限制,无法无限扩展,过多线程会导致 CPU 资源大量消耗在上下文切换,反而降低整体性能。

  1. WebFlux:WebFlux 采用事件循环模型,通过非阻塞 I/O 操作处理多个请求,无需为每个请求分配专用线程。在压测中,使用 WebClient 进行非阻塞 Http 调用,并通过 RouterFunction 进行请求映射和处理。最终压测结果显示,仅用 25 个线程就实现了每秒 964 个请求的吞吐量,请求处理完全无阻塞。

  2. Tomcat + 虚拟线程池:虚拟线程内存占用低,可大量创建且在遇到阻塞操作时自动卸载,提升平台线程利用率。使用虚拟线程时,需在启动参数中添加--enable-preview,并将 Tomcat 的线程池替换为虚拟线程池。

@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandler() {return protocolHandler ->protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
}private final RestTemplate restTemplate;
@GetMapping
public ResponseEntity<Object> callSlowServer() {return restTemplate.getForEntity("http://127.0.0.1:8000", Object.class);
}

压测结果显示,虚拟线程实现了与 WebFlux 相同的性能表现,但无需使用复杂的响应式编程技术,对慢速服务器的调用仍采用常规阻塞的 RestTemplate,仅通过替换线程池就达到了显著的性能提升效果。

六、总结

传统服务端开发采用“一请求一线程”模型,但平台线程成本高、数量有限,难以应对高并发场景。为此,业界转向非阻塞I/O和异步编程框架(如WebFlux、RX-Java),通过线程复用提升吞吐量,但带来了响应式编程的复杂性和高学习成本。

虚拟线程的出现改变了这一局面,开发者无需改变编程习惯,仍以同步阻塞的方式编写代码,即可自动获得高并发能力,大幅降低了高并发、高吞吐应用的开发门槛。

相关文章:

  • 【网页端数字人开发】基于模型SAiD实现嘴型同步
  • ShaderToy:入门
  • for(;;) 和while(1) 的无限循环用法对比,优缺点说明
  • Python训练营-Day23-Pipeline
  • Caliper 配置文件解析:config.yaml 和 fisco-bcos.json 附加在caliper中执行不同的合约方法
  • const和constexpr详解
  • Android第十四次面试总结
  • 如何实现安卓端与苹果端互通的多种方案
  • SEO长尾关键词实战优化指南
  • mybatis的if判断==‘1‘不生效,改成‘1‘.toString()才生效的原因
  • 【MCP实践】Python构建MCP应用全攻略:从入门到实战
  • 2025五大免费变声器推荐!
  • C语言三位数倒序输出(两种方法)
  • 使用pwm控制一个舵机摆动的速度
  • 5.4.3树和森林的遍历
  • Caliper 配置文件解析:fisco-bcos.json
  • 极空间z4pro配置gitea mysql,内网穿透
  • 3.lombok
  • 汉诺塔问题深度解析
  • 【Block总结】EBlock,快速傅里叶变换(FFT)增强输入图像的幅度|即插即用|CVPR2025
  • 进一步强化网站建设/艺考培训
  • app开发与网站开发的区别/谷歌浏览器网址
  • 免费php空间/龙岗seo优化
  • 网站调用网页怎么做/北京建站工作室
  • 阜宁做网站哪家公司好/软文推广多少钱一篇
  • wordpress主题几个网站/品牌营销策略分析论文