# Java 并发速览:虚拟线程到底解决了什么问题
Java 并发速览:虚拟线程到底解决了什么问题
一、老问题的根源
传统 Java 的 Thread 是平台线程,1:1 映射操作系统线程。每个线程默认栈 1MB,上下文切换要进内核。
Web 服务处理一个请求,大部分时间在等 IO(数据库、HTTP 调用)。线程只是"占着位置干等"。要让并发数上去,就得加线程,但几千个线程就开始吃不消 —— 于是有了线程池,有了 CompletableFuture + 回调链,有了响应式框架(WebFlux)。
本质上,大家都在绕同一个弯:用复杂换取不阻塞线程。
二、虚拟线程的解法
Java 21 引入虚拟线程(JEP 444)。它由 JVM 调度,M:N 映射到少量平台线程(载体线程)。
关键机制:虚拟线程执行阻塞操作时,JVM 会把它从载体线程上卸载(unmount),载体线程立刻去跑别的虚拟线程。IO 完成后再挂回某个载体线程继续执行。
结果是:你可以直接用阻塞式代码,却几乎不消耗 OS 线程。
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i ->
executor.submit(() -> {
var body = httpClient.send(request, BodyHandlers.ofString()).body();
return process(body); // 阻塞 IO,但不会占住载体线程
})
);
}
一万个并发任务,照样跑。内存占用从 GB 级降到 MB 级。
三、三个必须记住的实践要点
1. 不要池化虚拟线程
虚拟线程创建极廉价,池化反而有害。Executors.newVirtualThreadPerTaskExecutor() 就是标准用法,用完就扔。
2. 限流改用信号量
以前线程池大小就是并发闸门。虚拟线程下,闸门要挪到外部资源上:
var sem = new Semaphore(50); // 数据库连接池只有 50
sem.acquire();
try {
return queryDatabase(); // 保护的是 DB,不是线程
} finally {
sem.release();
}
3. 警惕 pinning(钉住)
在 synchronized 块内发生阻塞 IO,虚拟线程会被钉在载体线程上无法卸载。改用 ReentrantLock,或者把它纳入 synchronized 之外。JDK 24 已基本修复(JEP 491),但老版本需要注意。
// 不好:会 pin
synchronized (lock) { blockingIoCall(); }
// 好
lock.lock();
try { blockingIoCall(); } finally { lock.unlock(); }
四、什么时候不该用
- CPU 密集型任务:虚拟线程对纯计算毫无帮助,老老实实用
newFixedThreadPool(CPU 核数)。 - 依赖 ThreadLocal 做对象池:比如池化的
ByteBuffer缓存,虚拟线程下会退化成"一线程一对象"。 - 已有响应式代码且运行良好:不必为了迁移而迁移。
五、一句话总结
虚拟线程最大的价值不是"更快",而是让你写回简单的顺序代码。Spring Boot 3.2+ 只需一行配置即可开启:
spring.threads.virtual.enabled=true
``
- 点赞
- 收藏
- 关注作者
评论(0)