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()

执行流程拆解

  1. 第一次 c.run()
    • 执行 Start C1
    • 遇到 Continuation.yield()暂停执行,保存当前栈状态
    • run() 方法返回,控制权交还主循环
  2. 第二次 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 并发工具包,它们与虚拟线程将长期共存,各司其职。


原始标题:Difference Between Thread and Virtual Thread in Java | Baeldung