如果说现代 Java 有一件"等了二十年"的事,那就是廉价的大并发。从 JDK 1.0 起,Java 的线程就直接映射操作系统线程(1:1 模型),一个线程要占 1MB 栈、创建贵、调度重,高并发只能靠线程池"省着用"。JDK 19 预览的**虚拟线程(Virtual Threads)**打破了这个天花板——它是 Project Loom 的结晶,让"百万级并发"变得像写同步代码一样简单。
这一篇讲虚拟线程是什么、为什么快、预览期怎么用,以及它如何改变你写并发的方式。
一、为什么传统线程不够用
操作系统线程(平台线程)的两个硬伤:
- 贵:默认栈 1MB,百万线程就是 1TB 内存,根本扛不住。
- 阻塞即浪费:一旦
Thread.sleep、数据库查询、网络 IO 阻塞,这个 OS 线程就被挂起,CPU 干等,吞吐量被卡在"线程数"上。
所以长久以来的解法都是线程池 + 异步回调:用少量平台线程跑很多任务,用CompletableFuture、响应式(Reactor/RxJava)避免阻塞。但异步回调把代码切成"意大利面条",可读性和调试都惨。
虚拟线程的逻辑正好反过来:让"每个任务一个线程"重新变便宜,于是你又能写直白的同步代码,又不牺牲吞吐。
二、虚拟线程是什么
虚拟线程是JDK 管理的轻量级线程,不绑定固定 OS 线程:
- 由 JVM 调度,挂在少量"载体线程(carrier,即平台线程)"上运行。
- 遇到阻塞(IO、
BlockingQueue.take、锁等待等挂载点),虚拟线程从载体上"卸载",载体去跑别的虚拟线程;阻塞结束再"挂载"回来。 - 内存占用极小(初始几 KB,按需增长),创建成本接近零,可以大量创建(百万级)。
一句话:虚拟线程 = 便宜到可以"一个请求一个线程"的并发单元,且阻塞不浪费载体。
三、怎么用(预览期写法)
JDK 19 预览,需--enable-preview:
// 创建并启动一个虚拟线程Threadvt=Thread.ofVirtual().name("task-",0).start(()->{System.out.println("跑在虚拟线程上:"+Thread.currentThread());});vt.join();// 大量并发:每个任务一个虚拟线程,毫无压力try(varexecutor=Executors.newVirtualThreadPerTaskExecutor()){for(inti=0;i<1_000_000;i++){executor.submit(()->{Thread.sleep(1000);// 阻塞?没问题,载体去跑别的returndoWork();});}}// 关闭即等待全部完成注意Executors.newVirtualThreadPerTaskExecutor():它不为每任务真开 OS 线程,而是为每个任务起一个虚拟线程。对比传统newFixedThreadPool(200)只能 200 并发,这个执行器能轻松百万任务并发,且代码还是同步写法(sleep、顺序调用),没有回调地狱。
四、为什么虚拟线程能"既便宜又不阻塞"
关键在于JVM 识别挂载点(pin 点)。当虚拟线程执行到会阻塞的操作(大多数是 JDK 内部的 IO、锁、Object.wait),JVM 自动把它从载体线程卸载;阻塞解除再调度回任意空闲载体。
但要注意一个坑——synchronized 会"钉住"(pin)虚拟线程:在synchronized块内阻塞时,虚拟线程暂时无法卸载,会占着载体,退化成近似平台线程。所以虚拟线程的最佳拍档是java.util.concurrent.locks.ReentrantLock(它配合虚拟线程能正确卸载):
privatefinalReentrantLocklock=newReentrantLock();voidwork(){lock.lock();// 虚拟线程在此阻塞可正常卸载try{/* 临界区 */}finally{lock.unlock();}}经验:用虚拟线程时,把热路径上的
synchronized换成ReentrantLock,避免 pin 影响吞吐。
五、虚拟线程不是银弹
必须清醒认识边界:
- CPU 密集型任务不受益:虚拟线程优化的是"等待",不是"计算"。纯计算任务多了虚拟线程只是多了调度开销,该用线程池算还是算。
- 不要池化虚拟线程:虚拟线程本就极廉价,再套线程池是画蛇添足(直接用
newVirtualThreadPerTaskExecutor)。 - thread-local 语义变了:海量虚拟线程下,thread-local 会变成内存负担;JDK 21 引入"作用域值(ScopedValue)"作为替代。
- 调试视角变了:百万虚拟线程,传统线程 dump 会爆,需用 JDK 新的工具(
jcmd的虚拟线程视图)。
六、顺带:JDK 18/19 其他特性
| 版本 | 特性 | 说明 |
|---|---|---|
| 18 | UTF-8 默认字符集 | 跨平台不再因默认编码不一致翻车 |
| 18 | 简单 Web 服务器jwebserver | 临时静态服务零依赖 |
| 18 | 代码片段 API(JEP 413) | 文档里嵌入可执行代码 |
| 19 | 结构化并发(预览) | 把一组相关任务当"一个单元"管理(见下) |
| 19 | 外部函数与内存 API(预览) | Panama 项目,替代 JNI 调本地代码 |
| 19 | Vector API(孵化) | SIMD 向量计算加速 |
**结构化并发(Structured Concurrency)**和虚拟线程是搭档:它用StructuredTaskScope把"父任务派生的子任务"绑成树,父取消/异常时子任务自动取消,避免线程泄漏、异常吞噬——让并发代码像"try-with-resources"一样有边界。
try(varscope=newStructuredTaskScope.ShutdownOnFailure()){Future<String>u=scope.fork(()->fetchUser());Future<Integer>o=scope.fork(()->fetchOrder());scope.join();// 等所有子任务scope.throwIfFailed();// 任一失败则整体失败returnnewResult(u.resultNow(),o.resultNow());}七、本篇记住的清单
- 虚拟线程是 JVM 管理的轻量级线程,遇到阻塞自动从载体卸载,便宜到可"一任务一线程"。
- 用法:
Thread.ofVirtual().start(...)或Executors.newVirtualThreadPerTaskExecutor(),预览期需--enable-preview。 - 虚拟线程优化"等待"而非"计算",CPU 密集任务不受益,且不要池化。
synchronized会 pin 虚拟线程,热路径换ReentrantLock。- thread-local 在海量虚拟线程下是负担,JDK 21 用 ScopedValue 替代。
- 结构化并发
StructuredTaskScope把子任务绑成树,取消/异常自动传播。 - 18/19 还带来 UTF-8 默认编码、外部函数内存 API(Panama)、Vector API 等。
虚拟线程预览只是序幕。 JDK 21它正式转正,加上分代 ZGC 和一系列增强,成为"并发写法分水岭"的 LTS。