详解JDK21新特性【虚拟线程】

平台线程的局限

来看看官方文档对平台线程局限的解释:

服务器应用程序的可扩展性受利特尔定律(Little's Law)的约束,该定律关联延迟、并发性和吞吐量:对于给定的请求处理时长(即延迟),应用程序同时处理的请求数量(即并发性)必须与请求到达率(即吞吐量)成正比增长。例如,假设一个平均延迟为 50 毫秒的应用程序,通过并发处理 10 个请求,实现了每秒 200 个请求的吞吐量。为了使该应用程序的吞吐量扩展到每秒 2000 个请求,它需要并发处理 100 个请求。如果每个请求都在一个线程中处理,并且处理时间与请求时长相同,那么为了保证应用程序能够跟上吞吐量的增长,线程数必须随着吞吐量的增长而增长。

遗憾的是,可用线程数量有限,因为 JDK 将线程实现为对操作系统线程的封装。操作系统线程开销很大,因此我们不能拥有过多的线程,这使得这种实现方式并不适合每个请求一个线程的架构。如果每个请求在其持续时间内都占用一个线程(也就是一个操作系统线程),那么线程数量通常会在其他资源(例如 CPU 或网络连接)耗尽之前就成为限制因素。JDK 当前的线程实现将应用程序的吞吐量限制在一个远低于硬件支持的水平。即使使用线程池,这种情况仍然会发生,因为线程池虽然可以避免启动新线程的高昂开销,但并不会增加线程总数。

从日常业务出发,可以总结为四点:

1. 资源开销极高,数量受限

  • 内存成本:普通线程的栈空间是固定分配的(通常几 MB),且包含线程本地存储(ThreadLocal)、寄存器状态等元数据,创建大量线程会快速耗尽内存。例如,一个栈大小为 1MB 的线程,创建 10 万个就需要约 100GB 内存,这在实际系统中几乎不可行。
  • 调度成本 :普通线程由操作系统内核调度,调度时需要在用户态和内核态之间切换(上下文切换),开销大。操作系统能高效调度的线程数量有限,超过数量后,调度开销会导致系统性能急剧下降。

2. I/O 密集型任务中利用率极低

普通线程在执行 I/O 操作(如网络请求、数据库查询、文件读写)时会进入阻塞状态,此时操作系统会将其挂起,但其绑定的内核线程仍被占用(无法执行其他任务),导致资源浪费。

3. 高并发场景下编程模型复杂

为突破普通线程的数量限制,开发者被迫采用异步编程模型 (如回调、CompletableFuture、响应式编程等),通过 "少量线程 + 事件循环" 处理大量任务。但这种模型存在明显问题:

  • 代码复杂度高:异步代码通常是链式回调或基于状态机,逻辑分散,可读性和可维护性差(如 "回调地狱")。
  • 调试和监控困难:异步栈跟踪碎片化,日志和调试工具难以追踪任务执行流程;线程与任务的对应关系模糊,难以定位问题。

4. 与业务规模的扩展性冲突

随着业务增长,系统需要处理的并发任务数量可能从几千级增长到几十万级。普通线程由于数量限制,无法直接通过增加线程数来应对这种增长,必须进行架构改造,成本高且风险大。

虚拟线程的优势

  • 虚拟线程资源丰富且成本低廉 ,栈内存按需动态分配(初始仅几十 KB),且可自动伸缩,元数据由 JVM 管理,创建数百万甚至数千万个虚拟线程也不会耗尽内存;调度由 JVM 完成(用户态调度),上下文切换成本极低。
  • 当虚拟线程执行阻塞操作时,JVM 会挂起该虚拟线程,直到它可以恢复运行。与被挂起的虚拟线程关联的操作系统线程现在可以自由地为其他虚拟线程执行操作。
  • 完全兼容传统的同步编程模型(ThreadExecutorServicesynchronized 等),开发者可用简单的 "一个任务一个线程" 模式编写高并发代码,无需学习异步语法,调试和监控也与普通线程一致。
  • 其数量几乎不受限(仅受内存限制),可直接通过增加虚拟线程数应对并发增长,无需重构代码或架构,扩展性极强。

为什么虚拟线程不适合于CPU密集型的任务?

  • 虚拟线程最终依赖平台线程执行计算
  • CPU 密集型任务的瓶颈是 CPU 核心数,而非线程数量
  • 额外的调度开销反而降低效率

虚拟线程的底层

虚拟线程是混合实现 的。将内核线程与用户线程一起使用的实现方式,被称为N:M实现。

用户线程还是完全建立在用户空间 中,因此用户线程的操作依然廉价,并且可以支持大规模的用户线程并发。而操作系统支持的轻量级进程则作为用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级进程来完成,这大大降低了整个进程被完全阻塞的风险。

抢占式调度:虚拟线程的调度由 JVM 自动完成,类似内核线程的抢占式调度(无需开发者显式干预),当虚拟线程执行阻塞操作(如 I/O)时,JVM 会自动将其挂起,并将平台线程让给其他虚拟线程。

如何使用虚拟线程

虚拟线程的创建十分简单,就拿我最近练习的项目举例:

只需要 Thread.ofVirtual().start()就可以创建一个虚拟线程并执行任务了。

还有更加灵活的用法:

java 复制代码
// 构建虚拟线程(未启动)
Thread virtualThread = Thread.ofVirtual()
        .name("my-virtual-thread-1") // 设置线程名
        .priority(Thread.NORM_PRIORITY) // 设置优先级(仅为提示,JVM 可能忽略)
        .unstarted(() -> { // 任务逻辑
            System.out.println("Task running in: " + Thread.currentThread().getName());
        });

// 手动启动
virtualThread.start();
相关推荐
艾莉丝努力练剑2 小时前
【C++:红黑树】深入理解红黑树的平衡之道:从原理、变色、旋转到完整实现代码
大数据·开发语言·c++·人工智能·红黑树
No0d1es2 小时前
电子学会青少年软件编程(C/C++)1级等级考试真题试卷(2025年9月)
java·c语言·c++·青少年编程·电子学会·真题·一级
l1t2 小时前
利用DeepSeek优化SQLite求解数独SQL用于DuckDB
开发语言·数据库·sql·sqlite·duckdb
_OP_CHEN2 小时前
C++进阶:(七)红黑树深度解析与 C++ 实现
开发语言·数据结构·c++·stl·红黑树·红黑树的旋转·红黑树的平衡调整
9号达人2 小时前
普通公司对账系统的现实困境与解决方案
java·后端·面试
硅农深芯2 小时前
如何使用ptqt5实现进度条的动态显示
开发语言·python·qt
超级苦力怕2 小时前
Java 为何 long a = 999999999 能过;long a = 9999999999 报错?一文讲透“宽化转换”
java
佐杰3 小时前
Jenkins使用指南1
java·运维·jenkins
dllxhcjla3 小时前
三大特性+盒子模型
java·前端·css