Java 21 引入了虚拟线程这一重磅特性——长期以来开发者们一直期待的“Java 协程”。在高并发和 I/O 密集型应用中,虚拟线程不仅大幅降低内存占用,还能实现高效的上下文切换,从而显著提升系统性能和吞吐量。
1. 什么是虚拟线程
虚拟线程是 JDK 21 推出的一种轻量级线程,其核心优势在于:
- 内存占用低:无需为每个虚拟线程分配一个独立的操作系统线程,降低了系统资源消耗。
- 高效上下文切换:由 JVM 内部调度管理,不涉及昂贵的 OS 级别线程切换,能更好地应对高并发场景。
简而言之,虚拟线程可看作是“任务(Task)”,它们运行在传统的操作系统线程之上,但在代码层面与常规线程使用方式完全一致。
2. 虚拟线程的工作原理
当应用程序启动一个虚拟线程时,JVM 会将其交由底层的线程池(由传统线程构成)执行。其核心工作机制包括:
-
任务调度:一个传统线程可以轮流执行多个虚拟线程。举例来说,假设系统创建了 1000 个虚拟线程,而底层线程池只有 10 个传统线程,那么:
- 初始时,V1 到 V10 分别调度到 T1 到 T10 上执行。
- 当虚拟线程(例如 V3)因 I/O 操作而阻塞时,T3 立即释放出来去执行等待中的虚拟线程(如 V11)。
- 如果所有虚拟线程均处于非阻塞状态,JVM 会按照时间片(例如每 100ns)轮转调度,将部分虚拟线程挂起以让新任务得以执行。
- 如果以上条件均不满足,新任务将挂起等待空闲传统线程。
-
阻塞处理:当虚拟线程遇到阻塞(如 I/O 操作)时,JVM 会立刻挂起该虚拟线程,并通过操作系统事件(如 epoll)通知 I/O 完成,从而在合适时机重新唤醒该虚拟线程。
这套机制确保了即使在大量 I/O 阻塞场景下,系统也不会因为传统线程资源不足而性能急剧下降。
3. 虚拟线程的调度
- 自动调度:JDK 21 默认启用虚拟线程,且调度由 JVM 管理。默认情况下,JVM 会利用 ForkJoinPool 来执行虚拟线程,并根据实际任务数动态调整底层线程数。
- 自定义调度:虽然大部分场景下无需手动干预,但若有特殊需求(例如控制并发量、定制线程池参数等),可自定义线程池并将虚拟线程交给该线程池执行。
4. 虚拟线程与传统线程的区别
两者的主要差异包括:
-
线程创建方式
- 虚拟线程:不直接创建操作系统线程,运行时由传统线程池调度。
- 传统线程:每创建一个线程,JVM 都会启动一个独立的操作系统线程。
-
资源消耗
- 虚拟线程:内存开销极小,可轻松创建百万级虚拟线程。
- 传统线程:资源开销较大,一般只能支持几千个线程。
-
上下文切换
- 虚拟线程:上下文切换由 JVM 管理,开销低。
- 传统线程:依赖 OS 级调度,切换开销较高。
-
调度与执行
- 虚拟线程:任务调度完全由 JVM 控制,遇到阻塞时只挂起任务,不会占用底层线程。
- 传统线程:阻塞操作会直接占用线程,影响线程池整体吞吐。
5. 虚拟线程与协程的对比
虽然虚拟线程与 Python 等语言中的协程在处理 I/O 阻塞时有相似的“让步”机制,但二者存在显著区别:
对比维度 | 虚拟线程 | 协程 |
---|---|---|
并发/并行 | 可在多个 CPU 上并行运行,支持真正的并行执行 | 只有单个主线程调度,同一时刻只处理一个任务 |
资源争夺 | 存在资源竞争和状态同步问题,需要合理设计并发控制 | 单线程执行,无并发资源争夺问题 |
框架支持 | JDK 21 原生支持,无需额外框架 | 依赖专用异步框架,编写及调试相对复杂 |
6. 如何使用虚拟线程
在 JDK 21 中,使用虚拟线程主要有两种方式:
6.1 直接创建并启动虚拟线程
public class VirtualThreadExample {
public static void main(String[] args) {
Thread virtualThread = Thread.ofVirtual().start(() -> {
System.out.println("Hello virtual thread");
});
try {
virtualThread.join(); // 等待虚拟线程完成
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
6.2 通过线程池执行虚拟线程
import java.util.concurrent.*;
public class VirtualThreadPoolExample {
public static void main(String[] args) {
// 创建一个虚拟线程池
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
// 提交多个任务到线程池
for (int i = 0; i < 10; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId + " running in " + Thread.currentThread());
});
}
// 关闭线程池
executor.shutdown();
}
}
注意:虚拟线程池不支持设置核心线程数、最大线程数或任务队列等参数。如果需要对并发量进行严格控制,可以自定义线程池。
7. 自定义虚拟线程池示例
为了避免因无限制并发导致 OOM 或对下游系统产生巨大压力,可借助信号量(Semaphore)和阻塞队列实现自定义虚拟线程池。下面是一个示例:
package com.zengbiaobiao.demo.vitrualthreaddemo;
import java