1. 引言
本文将深入探讨 Java 中传统线程(Kernel Thread)与 Project Loom 引入的虚拟线程(Virtual Thread)之间的核心差异。
随后,我们会介绍虚拟线程的典型使用场景,以及 Loom 项目带来的新 API。
⚠️ 需要特别说明的是:Project Loom 仍处于积极开发阶段。本文示例基于早期访问版本的 Loom VM(如 openjdk-15-loom+4-55_windows-x64_bin)运行。后续构建版本可能会对现有 API 进行重大变更甚至破坏兼容性。
值得一提的是,Loom 的 API 已经历一次重大调整:原先使用的 java.lang.Fiber
类已被移除,取而代之的是新的 java.lang.VirtualThread
类。
2. 线程 vs 虚拟线程:高层对比
从宏观角度看,两者的关键区别在于调度管理方:
- ✅ 传统线程:由操作系统内核直接管理和调度
- ✅ 虚拟线程:由 JVM(Java 虚拟机) 自行管理和调度
这个根本差异带来了以下连锁反应:
传统线程的痛点
- ❌ 创建成本高:每次创建新内核线程都需要一次系统调用(system call),开销较大
- ❌ 资源消耗大:每个线程默认占用约 1MB 栈空间,大量线程会导致内存压力
- ❌ 上下文切换昂贵:线程切换依赖操作系统调度,涉及用户态/内核态切换,性能损耗明显
- ❌ 阻塞即浪费:一旦线程因 I/O 阻塞,整个内核线程就被挂起,无法执行其他任务
正因如此,我们才广泛使用线程池来复用线程,避免频繁创建销毁。而为了提升吞吐,又不得不转向 NIO、异步回调等复杂模型,导致代码可读性和调试难度飙升——典型的“为了性能牺牲可维护性”踩坑现场。
虚拟线程的优势
- ✅ 创建极快:无需系统调用,JVM 直接在用户态完成分配
- ✅ 内存占用小:初始栈仅几 KB,按需动态扩展
- ✅ 无 OS 上下文切换:调度由 JVM 完成,避免昂贵的内核介入
- ✅ 天然抗阻塞:虚拟线程运行在“载体线程(Carrier Thread)”之上。当虚拟线程阻塞时,JVM 会自动将其挂起,并调度其他虚拟线程继续使用该载体线程
这意味着:
- 你可以轻松创建数百万虚拟线程而不会压垮系统
- 编写阻塞式代码也能获得接近异步非阻塞的吞吐量
- 代码回归直观的同步风格,告别“回调地狱”
⚠️ 例外情况:如果虚拟线程调用了本地方法(native method)并在此方法中发生阻塞,仍可能导致载体线程被占用。这是目前需要警惕的少数性能陷阱之一。
3. 新的线程构建器 API
Loom 在 Thread
类中引入了全新的构建器(Builder)API,极大简化了线程创建。来看示例:
Runnable printThread = () -> System.out.println(Thread.currentThread());
ThreadFactory virtualThreadFactory = Thread.builder().virtual().factory();
ThreadFactory kernelThreadFactory = Thread.builder().factory();
Thread virtualThread = virtualThreadFactory.newThread(printThread);
Thread kernelThread = kernelThreadFactory.newThread(printThread);
virtualThread.start();
kernelThread.start();
输出结果:
Thread[Thread-0,5,main]
VirtualThread[<unnamed>,ForkJoinPool-1-worker-3,CarrierThreads]
输出解析
- 第一行是传统线程的标准
toString()
输出 - 第二行显示虚拟线程:
- 名称未指定,显示为
<unnamed>
- 正在
ForkJoinPool
的某个 worker 线程上执行 - 所属线程组为
CarrierThreads
(载体线程组)
- 名称未指定,显示为
关键启示
- ✅ API 完全兼容:无论底层是内核线程还是虚拟线程,编程接口一致
- ✅ 无缝迁移:现有阻塞式代码无需改写即可运行在虚拟线程上
- ✅ 零学习成本:无需掌握全新概念,上手简单粗暴
4. 虚拟线程的底层机制:Continuation
虚拟线程的本质是 Continuation(延续) + Scheduler(调度器) 的组合。
- ✅ Continuation:可暂停、可恢复的执行单元,类似协程(Coroutine)
- ✅ Scheduler:负责调度 Continuation 的执行器,通常实现为
Executor
接口
默认情况下,Loom 使用 ForkJoinPool
作为调度器。
📌 注意:Continuation 是低层 API,日常开发应优先使用
Thread.builder()
这类高层 API。
但为了理解其工作原理,我们来看一个 Continuation 的实验性示例:
var scope = new ContinuationScope("C1");
var c = new Continuation(scope, () -> {
System.out.println("Start C1");
Continuation.yield(scope);
System.out.println("End C1");
});
while (!c.isDone()) {
System.out.println("Start run()");
c.run();
System.out.println("End run()");
}
输出:
Start run()
Start C1
End run()
Start run()
End C1
End run()
执行流程拆解
- 第一次
c.run()
:- 执行
Start C1
- 遇到
Continuation.yield()
,暂停执行,保存当前栈状态 run()
方法返回,控制权交还主循环
- 执行
- 第二次
c.run()
:- JVM 恢复之前保存的执行状态
- 从
yield
后继续执行,打印End C1
- 执行完成,
c.isDone()
返回 true
核心机制
- 当虚拟线程遇到阻塞操作(如 I/O)时,其内部的 Continuation 会自动
yield
- JVM 保存当前执行上下文,释放载体线程
- 载体线程立即被用于执行其他虚拟线程
- 阻塞结束后,JVM 恢复该 Continuation,从断点继续执行
这正是虚拟线程实现“高并发 + 阻塞式编程”共存的底层魔法。
5. 总结
本文核心要点归纳如下:
特性 | 传统线程 | 虚拟线程 |
---|---|---|
调度方 | 操作系统 | JVM |
创建成本 | 高(系统调用) | 极低(用户态) |
内存占用 | ~1MB/线程 | KB 级,动态扩展 |
阻塞影响 | 阻塞整个内核线程 | 仅挂起虚拟线程,载体线程可复用 |
编程模型 | 需异步/非阻塞 | 可直接使用阻塞 API |
适用场景 | CPU 密集型 | I/O 密集型(如 Web 服务) |
✅ 虚拟线程不是要取代传统线程,而是为高并发 I/O 场景提供更优雅的解决方案。
你可以通过访问 Loom 早期试用版 进一步探索。同时,也不要忽视已成熟的 Java 并发工具包,它们与虚拟线程将长期共存,各司其职。