Java 21 新特性:虚拟线程
Java 21 是继 Java 17 之后的新一代长期支持版本(LTS)。在 Java 21 的众多新特性中,最值得我们求职者关注的就是虚拟线程(Virtual Thread)。
虚拟线程并不是一种让 CPU 计算变快的“超级线程”,它主要解决的是:
在大量请求都需要等待数据库、网络接口等 I/O 操作时,传统线程数量不够用、创建成本又太高的问题。
本文先用人话解释虚拟线程为什么出现、它是怎么运行的,然后再介绍具体用法和注意事项。最后一笔带过Java 21 的其他常用特性。
一、先理解三种线程
学习虚拟线程之前,需要先分清操作系统线程、平台线程和虚拟线程。
1.操作系统线程
操作系统线程是由 Windows、Linux 等操作系统直接管理的线程。
它的特点是“比较重”:
创建和销毁需要操作系统参与。
每个线程都要占用一定内存。
线程切换需要进入操作系统内核,存在性能成本。
单个进程能够创建的线程数量有限,具体上限受内存、操作系统配置等因素影响。
我们可以把操作系统线程理解成饭店里的真实服务员。招聘一个服务员需要成本,饭店也不可能同时雇佣几十万个服务员。
2.平台线程
在以前的 Java 中,通过new Thread()创建的线程通常是平台线程:
Thread thread = new Thread(() -> { System.out.println("执行任务"); }); thread.start();平台线程由 JVM 提供给 Java 程序使用,但底层通常对应一个操作系统线程:
Java 平台线程 ↓ 操作系统线程因此,平台线程实际上是 JVM 对操作系统线程的一层包装。操作系统线程比较昂贵,平台线程自然也不能无限创建。
传统项目通常会创建线程池,让少量平台线程反复处理任务:
ExecutorService pool = Executors.newFixedThreadPool(200);相当于饭店只雇佣 200 个服务员,让大量订单排队等待这 200 个服务员处理。
3.虚拟线程
虚拟线程是由 JVM 管理的轻量级线程,在 JDK 19 中首次以预览特性出现,并在 JDK 21 中正式发布。
它和平台线程最大的区别是:
传统方式:一个平台线程通常对应一个操作系统线程 虚拟线程:大量虚拟线程复用少量平台线程可以把它们理解成:
虚拟线程:一张订单
平台线程:真正处理订单的服务员
JVM:负责派发订单的经理
操作系统线程:服务员实际占用的工作岗位
订单可以有几十万张,但并不需要几十万个服务员。JVM 会安排少量平台线程轮流执行大量虚拟线程。
二、为什么需要虚拟线程?
1.传统“一请求一线程”模型的问题
传统 Web 服务器经常采用“一次请求对应一个线程”的处理模型:
请求 1 → 平台线程 1 请求 2 → 平台线程 2 请求 3 → 平台线程 3这种模型很好理解,代码也容易编写。但平台线程数量有限,当请求量很大时,线程资源可能成为系统瓶颈。
更麻烦的是,大部分后端请求并不是一直在使用 CPU,而是在等待:
接收请求 ↓ 查询数据库 ↓ 等待数据库返回 ← 大部分时间可能花在这里 ↓ 处理查询结果 ↓ 返回响应假设一次数据库查询需要等待两秒。使用传统平台线程时,这个线程可能会被当前请求占用两秒,但在等待期间它并没有真正执行多少计算。
如果同时有大量请求都在等待数据库或远程接口,就会出现:
大量平台线程处于阻塞等待状态。
线程占用了内存,却没有充分使用 CPU。
请求继续增加后,只能在线程池队列里排队。
线程过多还会增加操作系统的调度和切换成本。
2.虚拟线程如何解决?
虚拟线程执行到可识别的阻塞 I/O 操作时,JVM 可以暂时把它从当前平台线程上卸载下来。
虚拟线程 A:发起数据库查询,需要等待 ↓ JVM 保存虚拟线程 A 当前执行到哪里 ↓ 平台线程空出来,开始执行虚拟线程 B ↓ 数据库结果返回后,再继续执行虚拟线程 A用饭店来比喻:
服务员把菜单交给厨房后,不需要站在厨房门口一直等。他可以先去处理其他订单,等厨房做好以后再回来继续处理原来的订单。
因此,少量平台线程可以支撑大量正在等待 I/O 的虚拟线程。
整体关系大致如下:
虚拟线程 1 ─┐ 虚拟线程 2 ─┤ 虚拟线程 3 ─┼──→ 少量平台线程 ──→ 少量操作系统线程 虚拟线程 4 ─┤ 虚拟线程 5 ─┘这里负责实际执行虚拟线程的平台线程,也经常被称为载体线程(Carrier Thread)。
三、虚拟线程是如何切换的?
线程要想暂停后继续运行,系统必须知道它刚才执行到了哪里,并保存相关数据,例如:
方法调用到了哪一层
方法中的局部变量
下一条要执行的指令位置
这些信息可以简单理解为线程的“工作现场”。
当虚拟线程因为 I/O 等待而暂停时,JVM 会保存它的工作现场,让平台线程去执行其他虚拟线程;等条件满足后,JVM 再恢复这个虚拟线程。
因此,虚拟线程的切换主要由 JVM 在用户态完成,不需要每次都让操作系统切换一个重量级线程,成本通常更低。
不过需要注意:
虚拟线程只是共享平台线程,并不是所有虚拟线程都能在同一时刻并行运行。
真正同时运行多少任务,最终仍然受到 CPU 核心数和平台线程数量限制。虚拟线程的优势是让平台线程在其他任务等待 I/O 时不闲着。
四、如何创建虚拟线程?
Java 21 提供了多种创建虚拟线程的方式。
1.直接启动虚拟线程
public class VirtualThreadDemo { public static void main(String[] args) throws InterruptedException { Thread thread = Thread.startVirtualThread(() -> { System.out.println("当前线程:" + Thread.currentThread()); }); thread.join(); } }Thread.startVirtualThread()会创建并立即启动一个虚拟线程。
2.使用Thread.ofVirtual()
public class VirtualThreadBuilderDemo { public static void main(String[] args) throws InterruptedException { Thread thread = Thread.ofVirtual() .name("order-task") .start(() -> System.out.println("处理订单")); thread.join(); } }这种方式可以为虚拟线程设置名称,方便日志记录和问题排查。
3.使用虚拟线程执行器
在实际项目中,更常见的方式是使用Executors.newVirtualThreadPerTaskExecutor():
import java.util.concurrent.Executors; import java.util.stream.IntStream; public class VirtualThreadExecutorDemo { public static void main(String[] args) { try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { IntStream.range(0, 10_000).forEach(i -> executor.submit(() -> { Thread.sleep(1_000); System.out.println("任务 " + i + " 执行完成"); return i; }) ); } } }这段代码会为每个任务创建一个虚拟线程。虽然提交了 10,000 个任务,但并不意味着操作系统会创建 10,000 个线程。
虚拟线程执行器实现了AutoCloseable,离开try代码块时会等待已提交任务结束。
五、平台线程和虚拟线程的区别
| 对比项 | 平台线程 | 虚拟线程 |
| 管理者 | 操作系统为主 | JVM 为主 |
| 与操作系统线程的关系 | 通常近似一对一 | 多个虚拟线程复用少量平台线程 |
| 创建成本 | 较高 | 较低 |
| 内存占用 | 较大 | 较小,并可按需增长 |
| 可创建数量 | 相对有限 | 可以创建非常多 |
| 是否需要线程池复用 | 通常需要 | 通常不需要 |
| 适合场景 | 通用,尤其是固定数量的计算任务 | 大量并发、阻塞式 I/O 任务 |
这里的“可以创建非常多”不等于完全没有成本。每个虚拟线程仍然需要保存自己的状态,创建数量最终仍受可用内存等资源限制。
六、虚拟线程适合哪些场景?
虚拟线程特别适合任务数量多,而且任务经常等待 I/O 的场景。
适合的场景
Web 请求处理
JDBC 数据库查询
HTTP、RPC 远程调用
文件读写
消息消费
大量需要等待结果的独立任务
例如一个订单接口需要依次查询数据库、调用库存服务和支付服务,大部分时间都在等待外部结果,这种场景很适合虚拟线程。
不会明显受益的场景
视频编码
图片压缩
密码计算
大规模数学运算
其他长时间占用 CPU 的任务
如果任务一直在计算,没有阻塞等待,虚拟线程就没有机会卸载并让出平台线程。
例如一台机器只有 8 个 CPU 核心,即使创建 10 万个虚拟线程执行纯计算,也不可能让 10 万个任务真正同时计算。任务反而可能因为频繁调度而变慢。
所以要记住:
虚拟线程解决的是“线程被大量等待占住”的问题,不是“CPU 不够快”的问题。
七、虚拟线程常见误区
误区 1:虚拟线程比普通线程执行得更快
不准确。执行同一段纯计算代码时,虚拟线程不一定更快。它的优势是等待 I/O 时可以释放载体线程,从而提高系统吞吐量。
误区 2:有了虚拟线程,就不需要数据库连接池
不正确。数据库连接本身是昂贵且有限的资源,仍然需要连接池管理。
误区 3:虚拟线程数量没有上限
不正确。虚拟线程很轻,但仍然会占用内存和 JVM 管理资源,只是可创建数量通常远高于平台线程。
误区 4:虚拟线程等于异步编程
不完全相同。
虚拟线程允许开发者继续使用容易理解的同步阻塞代码:
User user = userService.findById(id); List<Order> orders = orderService.findByUserId(id); return new UserDetail(user, orders);代码看起来仍然是从上往下执行的,但线程在等待 I/O 时,JVM 可以在底层调度其他虚拟线程。
它的价值之一,就是在获得高并发能力的同时,避免把业务代码写成大量回调或复杂的异步调用链。
八、Java 21 的其他常用新特性
除了虚拟线程,Java 21 还有几个值得了解的正式特性。
1.switch模式匹配
可以直接根据对象类型进行分支判断:
static String format(Object value) { return switch (value) { case Integer i -> "整数:" + i; case String s -> "字符串:" + s; case null -> "空值"; default -> "其他类型"; }; }相比连续使用多个if instanceof,代码更加清晰。
2. 记录模式
可以直接拆解record中的数据:
record Point(int x, int y) {} static void print(Object value) { if (value instanceof Point(int x, int y)) { System.out.println("x=" + x + ", y=" + y); } }3. 顺序集合
Java 21 新增了SequencedCollection、SequencedSet和SequencedMap,统一提供首元素、尾元素和反向视图等操作:
List<String> list = new ArrayList<>(List.of("A", "B", "C")); list.addFirst("Start"); list.addLast("End"); System.out.println(list.getFirst()); System.out.println(list.getLast()); System.out.println(list.reversed());4. 分代 ZGC
Java 21 为 ZGC 引入分代回收能力。它根据“大多数新对象存活时间很短”的特点,对年轻对象和长期存活对象分别管理,从而降低部分场景下的垃圾回收成本。
Java 21 中可以通过以下参数启用:
java -XX:+UseZGC -XX:+ZGenerational MyApplication
是否切换垃圾收集器应以真实业务压测结果为准。
九、总结
虚拟线程的核心思想其实就是:
让大量轻量级虚拟线程复用少量重量级平台线程。当一个虚拟线程等待 I/O 时,平台线程可以先去执行其他虚拟线程。
最后总结一下:
平台线程通常接近一个 Java 线程对应一个操作系统线程,创建成本较高。
虚拟线程由 JVM 管理,大量虚拟线程可以复用少量平台线程。
虚拟线程适合大量数据库、网络、文件等阻塞 I/O 任务。
虚拟线程不会让纯 CPU 计算变快,也不会扩大数据库和下游服务的容量。
对于传统的“一请求一线程”Java Web 应用,虚拟线程降低了高并发编程的门槛。开发者可以继续使用直观的同步代码,同时减少大量平台线程阻塞等待造成的资源浪费。这也是虚拟线程成为 Java 21 最重要新特性之一的原因。
最后,如果你觉得本篇内容对你有帮助的话,欢迎点赞关注评论,谢谢⭐⭐⭐
