虚拟线程
[!IMPORTANT] 本系列以 JDK 20 为基准。虚拟线程在 JDK 20 是第二次预览特性,使用
Thread.ofVirtual或Executors.newVirtualThreadPerTaskExecutor的案例需要javac --release 20 --enable-preview Example.java和java --enable-preview Example。预览特性不是稳定 API,升级 JDK 时要重新核对。
javac --release 20 --enable-preview VirtualThreadDemo.java
java --enable-preview VirtualThreadDemo学习目标
- 区分平台线程与虚拟线程的创建成本、调度方式和工作负载边界。
- 使用每任务虚拟线程执行器处理大量等待型任务,并用 Semaphore 限制下游资源。
- 识别 pinning、ThreadLocal、线程泄漏和结构化并发的 JDK 20 状态。
核心知识点
专业术语
- 虚拟线程(virtual thread):由 JVM 调度、挂载到少量平台线程上的轻量线程。
- 载体线程(carrier thread):实际承载虚拟线程运行字节码的平台线程。
- pinning:虚拟线程阻塞时无法卸载载体线程的情况,常见于 synchronized 监视器或 native 调用。
- ThreadLocal:按线程保存值的局部变量机制;在线程数量很大时要关注每个任务的值和生命周期。
- 结构化并发(structured concurrency):把子任务的生命周期、取消和异常收束到词法作用域中的并发模型;JDK 20 为孵化 API。
白话解释与边界
虚拟线程便宜的是“等待时的线程状态”,不是 CPU,也不是数据库连接、文件句柄和远端 QPS。它特别适合每个请求都会阻塞等待 I/O 的代码;CPU 密集循环仍受核心数限制,应该使用有界平台线程池或其他并行策略。
虚拟线程不应被当作无限制并发开关。对数据库、HTTP 服务、文件系统等下游资源仍要使用连接池、Semaphore、超时和取消。通过 JFR 或线程转储验证 pinning,不要靠猜测换锁。
常用用法
Thread.ofVirtual:创建并等待虚拟线程
Thread.ofVirtual 用于创建等待型任务线程,在 JDK 20 中属于预览 API。
// 说明:Thread.ofVirtual:创建并等待虚拟线程。
public class VirtualThreadDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = Thread.ofVirtual()
.name("virtual-worker")
.start(() -> {
// 初始状态:thread = Thread.ofVirtual()。
// 作用:Thread thread = Thread.ofVirtual();创建并等待虚拟线程。
System.out.println(Thread.currentThread().isVirtual());
// 输出:true
});
thread.join();
// 作用:thread.join();;创建并等待虚拟线程。
}
}start 会立即安排任务,unstarted 只创建不启动;业务代码要像管理平台线程一样处理异常、取消和生命周期。
Executors.newVirtualThreadPerTaskExecutor:每任务一个虚拟线程
该执行器不提供传统固定线程池那样的并发上限;close() 会等待已提交任务完成。
// 说明:Executors.newVirtualThreadPerTaskExecutor:每任务一个虚拟线程。
import java.util.concurrent.Executors;
public class VirtualExecutorDemo {
public static void main(String[] args) throws Exception {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
// 初始状态:executor = Executors.newVirtualThreadPerTaskExecutor())。
// 作用:try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {;每任务一个虚拟线程。
var first = executor.submit(() -> "a");
// 作用:var first = executor.submit(() -> "a");;每任务一个虚拟线程。
// 初始状态:first = executor.submit(() -> "a")。
var second = executor.submit(() -> "b");
// 作用:var second = executor.submit(() -> "b");;每任务一个虚拟线程。
// 初始状态:second = executor.submit(() -> "b")。
System.out.println(first.get() + second.get());
// 输出:ab
}
}
}对外部资源需要另加 Semaphore 或连接池,而不是因为线程便宜就无限提交。
Semaphore:限制下游并发
许可证数量代表下游允许的并发量,而不是虚拟线程数量。
// 说明:Semaphore:限制下游并发。
import java.util.concurrent.Semaphore;
public class VirtualThreadLimitDemo {
public static void main(String[] args) throws InterruptedException {
Semaphore permits = new Semaphore(2);
// 初始状态:permits = new Semaphore(2)。
// 作用:Semaphore permits = new Semaphore(2);;限制下游并发。
permits.acquire();
// 作用:permits.acquire();;限制下游并发。
try {
System.out.println("one downstream slot");
// 输出:one downstream slot
} finally {
permits.release();
// 作用:permits.release();;限制下游并发。
}
}
}必须在 finally 中释放,异常或取消路径漏放会造成永久饥饿。
ThreadLocal:按任务保存小范围上下文
虚拟线程通常不是长寿命池线程,ThreadLocal 泄漏风险较小但仍应在任务边界清理;不要把大对象、连接或事务状态长期绑定在线程本地。
// 说明:ThreadLocal:按任务保存小范围上下文。
public class VirtualThreadLocalDemo {
private static final ThreadLocal<String> REQUEST_ID = new ThreadLocal<>();
// 初始状态:REQUEST_ID = new ThreadLocal<>()。
// 作用:private static final ThreadLocal<String> REQUEST_ID = new ThreadLocal<>();;按任务保存小范围上下文。
public static void main(String[] args) throws InterruptedException {
Thread thread = Thread.ofVirtual().start(() -> {
// 作用:Thread thread = Thread.ofVirtual().start(() -> {;按任务保存小范围上下文。
// 初始状态:thread = Thread.ofVirtual().start(() ->。
REQUEST_ID.set("req-1");
// 作用:REQUEST_ID.set("req-1");;按任务保存小范围上下文。
try {
System.out.println(REQUEST_ID.get());
// 输出:req-1
} finally {
REQUEST_ID.remove();
// 作用:REQUEST_ID.remove();;按任务保存小范围上下文。
}
});
thread.join();
// 作用:thread.join();;按任务保存小范围上下文。
}
}Thread.sleep:等待型工作不会占满载体
示例用 sleep 模拟等待,真实程序应使用带超时的阻塞 I/O。
// 说明:Thread.sleep:等待型工作不会占满载体。
public class VirtualSleepDemo {
public static void main(String[] args) throws Exception {
try (var executor = java.util.concurrent.Executors.newVirtualThreadPerTaskExecutor()) {
// 初始状态:executor = java.util.concurrent.Executors.newVirtualThreadPerTaskExecutor())。
var future = executor.submit(() -> {
// 作用:var future = executor.submit(() -> {;等待型工作不会占满载体。
// 初始状态:future = executor.submit(() ->。
Thread.sleep(1);
// 初始状态:Thread.sleep(1)。
// 作用:Thread.sleep(1);;等待型工作不会占满载体。
return "io-like wait";
});
System.out.println(future.get());
// 输出:io-like wait
}
}
}虚拟线程不是让 sleep 变快,而是让等待期间更少占用平台线程。
不常用但需要知道
pinning:监视器或 native 调用可能占住载体
JDK 20 需要通过 JFR 等证据确认 pinning;不要把所有 synchronized 都机械替换为 Lock。真正的改进通常是缩短锁内代码,把阻塞 I/O 移到临界区外。
// 说明:work() 持有 monitor 时只执行短输出;若把长时间 native 或阻塞调用放进该区域,虚拟线程可能占住载体线程。
public class PinningBoundaryDemo {
private final Object monitor = new Object();
// 初始状态:monitor = new Object()。
// 作用:private final Object monitor = new Object();;监视器或 native 调用可能占住载体。
void work() {
synchronized (monitor) {
// 这里若执行长时间阻塞调用,可能 pin 住载体线程
System.out.println("short critical section");
// 输出:short critical section
}
}
public static void main(String[] args) {
new PinningBoundaryDemo().work();
// 作用:new PinningBoundaryDemo().work();;监视器或 native 调用可能占住载体。
// 初始状态:new PinningBoundaryDemo().work()。
}
}结构化并发:JDK 20 孵化 API
JDK 20 的 jdk.incubator.concurrent 是孵化模块,不是稳定 API;只使用该 API 时编译和运行需分别加 --add-modules jdk.incubator.concurrent。它把子任务的等待、取消和异常收束到 scope,仍应核对目标 JDK 的具体签名。
只使用该孵化 API 时可以这样编译和运行:
import jdk.incubator.concurrent.StructuredTaskScope;
public class StructuredConcurrencyDemo {
public static void main(String[] args) throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
// 初始状态:scope = new StructuredTaskScope.ShutdownOnFailure())。
// 作用:try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {;JDK 20 孵化 API。
var left = scope.fork(() -> "left");
// 初始状态:left = scope.fork(() -> "left")。
// 作用:var left = scope.fork(() -> "left");;JDK 20 孵化 API。
var right = scope.fork(() -> "right");
// 初始状态:right = scope.fork(() -> "right")。
// 作用:var right = scope.fork(() -> "right");;JDK 20 孵化 API。
scope.join();
// 初始状态:scope.join()。
// 作用:scope.join();;JDK 20 孵化 API。
scope.throwIfFailed();
// 初始状态:scope.throwIfFailed()。
// 作用:scope.throwIfFailed();;JDK 20 孵化 API。
System.out.println(left.get() + right.get());
// 输出:leftright
}
}
}javac --release 20 --add-modules jdk.incubator.concurrent StructuredConcurrencyDemo.java
java --add-modules jdk.incubator.concurrent StructuredConcurrencyDemoThread.Builder:配置线程名称和启动方式
Builder 适合统一命名或配置未启动线程;大量任务通常直接使用每任务执行器,并把命名策略集中放在 ThreadFactory 或任务日志上下文中。
// 作用:通过 Thread.Builder 配置线程名称和启动方式。
public class VirtualThreadBuilderDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = Thread.ofVirtual().name("request-1").unstarted(
() -> {
// 初始状态:thread = Thread.ofVirtual().name("request-1").unstarted(。
// 作用:Thread thread = Thread.ofVirtual().name("request-1").unstarted(;配置线程名称和启动方式。
System.out.println(Thread.currentThread().getName());
// 输出:request-1
});
thread.start();
// 作用:thread.start();;配置线程名称和启动方式。
thread.join();
// 作用:thread.join();;配置线程名称和启动方式。
}
}简单案例
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
public class VirtualThreadServiceDemo {
public static void main(String[] args) throws Exception {
Semaphore limit = new Semaphore(1);
// 初始状态:limit = new Semaphore(1)。
// 作用:Semaphore limit = new Semaphore(1);;配置线程名称和启动方式。
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
// 初始状态:executor = Executors.newVirtualThreadPerTaskExecutor())。
var future = executor.submit(() -> {
// 作用:var future = executor.submit(() -> {;配置线程名称和启动方式。
// 初始状态:future = executor.submit(() ->。
limit.acquire();
// 作用:limit.acquire();;配置线程名称和启动方式。
try {
return "response";
} finally {
limit.release();
// 作用:limit.release();;配置线程名称和启动方式。
}
});
System.out.println(future.get());
// 输出:response
}
}
}案例把“每请求一个虚拟线程”和“下游只允许一个并发”分开表达;线程扩展性与资源扩展性是两件事。
易混点
- 虚拟线程是 JDK 20 第二次预览,不是 JDK 20 的稳定特性;编译和运行都要
--enable-preview。 - 虚拟线程适合等待型工作,不会让 CPU 密集代码突破核心数,也不会增加数据库连接上限。
- pinning 是载体被占住,不是所有 synchronized 都有问题;应先用 JFR 定位长临界区或 native 阻塞。
- ThreadLocal 是线程局部,不是请求自动清理的上下文;任务边界仍需 remove。
- 结构化并发在 JDK 20 是孵化 API,命令需要
--add-modules,不能写成稳定标准库保证。
课后小问
为什么虚拟线程多并不代表数据库可以打开同样多的连接? 答案:虚拟线程只是执行调度资源,数据库连接是独立的有容量下游资源。 解析:使用连接池、Semaphore、超时和排队把请求并发限制在数据库可承受范围内。
看到 synchronized 就应该改成 ReentrantLock 吗? 答案:不应该,只有明确需要可中断、超时、多个 Condition 或诊断控制时才考虑 Lock。 解析:synchronized 释放更可靠;改用 Lock 反而增加了手动 unlock 和遗漏异常路径的风险。
JDK 20 运行结构化并发示例需要哪些参数? 答案:使用
jdk.incubator.concurrent时编译和运行都要加--add-modules jdk.incubator.concurrent。 解析:它是孵化 API,不是预览语法;若另外使用预览语法,才在两侧再加--enable-preview。
本节小结
- 虚拟线程降低大量等待型任务的线程管理成本,但不改变 CPU 和外部资源上限。
- 每任务虚拟线程执行器适合简单请求并发,连接池、Semaphore 和超时负责背压。
- pinning、ThreadLocal 和取消仍是普通线程同样重要的生命周期问题。
- JDK 20 的虚拟线程是预览特性,结构化并发是孵化 API,升级时必须重新核对。
快速回顾
- 会用
Thread.ofVirtual和newVirtualThreadPerTaskExecutor。 - 能解释载体线程、pinning 和 CPU/IO 工作负载边界。
- 会用 Semaphore 限制虚拟线程访问下游资源。
- 能写出 JDK 20 预览与孵化特性的正确编译、运行参数。
阿源的知识库