1. 概述

这篇文章带你快速了解 Project Loom。简单来说,Project Loom 的主要目标是探索、孵化并交付 Java 虚拟机的新特性及其上层 API,用于支持易用、高吞吐量的轻量级并发模型和新的编程范式

2. Project Loom 是什么?

Project Loom 是 OpenJDK 社区为 Java 引入轻量级并发机制的一次尝试。它计划通过在整个 JDK 中引入新特性、API 和优化来实现轻量级并发的广泛应用。Loom 的原型已经在 JVM 和 Java 类库中带来了变化。其中最核心的功能是 虚拟线程(virtual threads),该功能已经实现。

虽然目前还没有 JDK 版本完全集成 Loom,但我们可以通过 Project Loom Early-Access Builds 提前体验。

在深入 Loom 的各种概念之前,我们先来看看 Java 当前的并发模型。

3. Java 的并发模型

目前,Java 中的并发抽象核心是 Thread。借助 Thread 及其他 并发 API,我们可以轻松编写并发程序。通常,我们使用 Thread 创建 平台线程(platform threads),它们与操作系统内核线程一一对应(1:1)。操作系统为每个平台线程分配较大的栈空间和其他资源,但这些资源是有限的。

然而,由于 Java 并发模型依赖于 操作系统内核线程,在面对现代高并发场景时显得力不从心。主要存在两个问题:

  1. 线程数量无法匹配业务并发单位。例如,应用可能需要处理数百万的事务、用户或会话,但操作系统支持的线程数量远少于这个数量级。因此,为每个用户、事务或会话分配一个线程往往不可行。
  2. 线程间频繁同步导致上下文切换开销大。大多数并发应用在每次请求中都需要线程间同步,导致昂贵的内核线程上下文切换。

一种常见的解决方案是使用 异步并发 API,如 CompletableFutureRxJava。只要这些 API 不阻塞内核线程,就可以在 Java 线程之上提供更细粒度的并发控制。

但问题是,这类 API 调试困难,且难以与传统 API 集成。因此,我们需要一种与内核线程无关的轻量级并发机制。

4. 任务与调度器

无论是轻量级还是重量级线程,其实现都依赖两个核心组件:

  1. 任务(Task)(也称为 continuation):可以暂停自身以等待阻塞操作的一系列指令
  2. 调度器(Scheduler):负责将任务分配给 CPU,并在任务暂停时重新调度

目前,Java 的 continuation 和 scheduler 都依赖操作系统实现。

为了暂停一个 continuation,需要保存整个调用栈,并在恢复时重新加载。由于 OS 层的 continuation 包括本地调用栈和 Java 调用栈,因此开销较大。

更大的问题是使用了 OS 的调度器。因为调度器运行在内核态,无法区分线程的上下文,对所有 CPU 请求一视同仁。

这种调度方式 对 Java 应用来说并不是最优的

举个例子,一个应用线程处理请求后,将数据传递给另一个线程继续处理。如果这两个线程能调度到同一个 CPU 上会更高效。但由于调度器无法感知线程的上下文,这种调度优化很难实现。

Project Loom 提出的解决方案是使用 用户态线程,其 continuation 和 scheduler 由 Java 运行时实现,而不是依赖操作系统。

5. 虚拟线程(Virtual Threads)

OpenJDK 21 正式引入了 虚拟线程,并为现有 API(如 ThreadThreadFactory)提供了创建虚拟线程的接口。

5.1. 虚拟线程与平台线程有何不同?

特性 平台线程(Platform Thread) 虚拟线程(Virtual Thread)
调度 由操作系统调度 由 Java 运行时调度
线程模式 内核线程 用户态线程(基于 continuation)
默认命名 有默认名称 无默认名称(可设置)
线程优先级 可设置 固定(不可更改)
是否为守护线程 可设置 是(守护线程)

5.2. 虚拟线程的优缺点

✅ 优点:

  • 轻量级,资源占用少
  • 可轻松创建数百万个虚拟线程
  • 适合 I/O 密集型任务
  • 易于使用,与现有 API 兼容

❌ 缺点:

  • 不适合 CPU 密集型任务
  • 在同步块中会阻塞底层平台线程(线程 pinned)
  • 每个虚拟线程都使用 ThreadLocal 会消耗大量内存

5.3. 何时使用虚拟线程?

  • ✅ 适用于大多数时间处于阻塞状态的任务(如 I/O)
  • ❌ 不适用于长时间占用 CPU 的计算密集型任务

5.4. 如何创建虚拟线程?

有两种主要方式:

方式一:使用 ofVirtual().start()

Thread thread = Thread.ofVirtual().start(Runnable task);

方式二:使用 startVirtualThread()

Thread thread = Thread.startVirtualThread(Runnable task);

方式三:使用 ThreadFactory

ThreadFactory factory = Thread.ofVirtual().factory();
Thread thread = factory.newThread(Runnable task);

判断是否为虚拟线程:

boolean isThreadVirtual = thread.isVirtual();

返回 true 表示该线程是虚拟线程。

5.5. 虚拟线程是如何实现的?

虚拟线程通过少量的底层平台线程(称为 carrier threads)来实现。当发生 I/O 等阻塞操作时,虚拟线程可以被重新调度到其他 carrier threads 上运行。

虚拟线程的执行代码并不感知底层平台线程的存在。调用 currentThread() 时返回的是虚拟线程对象,而非底层平台线程。

6. 有界 Continuation(Delimited Continuations)

Continuation(协程)是一段顺序执行的指令序列,可以暂停并在后续恢复执行。

每个 continuation 都有入口点和挂起点。调用者可以暂停 continuation,并在后续恢复执行。

⚠️ 与传统方式不同的是,挂起/恢复操作现在发生在 Java 运行时中,而非操作系统,从而避免了昂贵的内核线程上下文切换。

Delimited continuation 是支持虚拟线程的关键技术,通常不需要暴露给开发者。以下是一个伪代码示例:

one() {  
    ... 
    two()
    ...
}

two() {
    ...
    suspend // 挂起点
    ... // 恢复点
}

main() {
    c = continuation(one) // 创建 continuation  
    c.continue() // 调用 continuation 
    c.continue() // 再次调用以恢复
}

为了支持 continuation 的调用栈管理,JVM 需要具备捕获、存储和恢复调用栈的能力。为此,Project Loom 引入了 Unwind-and-Invoke(UAI) 技术,允许将调用栈展开到某个点,然后调用指定方法。

7. ForkJoinPool 与自定义调度器支持

之前我们提到,操作系统调度器无法将相关线程调度到同一 CPU 上。

虽然 Project Loom 的目标是支持可插拔调度器,但目前默认使用的是 **异步模式下的 ForkJoinPool**。

OpenJDK 19 为 ForkJoinPool 增加了新功能,例如 setParallelism(int size) 用于控制并行度,从而管理工作线程的创建、使用和销毁。

ForkJoinPool 使用 工作窃取算法(work-stealing)。每个线程维护一个任务队列,从队列头部取任务执行。空闲线程不会阻塞等待任务,而是从其他线程队列的尾部“偷”任务。

在异步模式下,工作线程从其他队列头部窃取任务

此外,ForkJoinPool 会将一个任务创建的子任务加入本地队列,从而尽可能在同一 CPU 上执行。

8. 结构化并发(Structured Concurrency)

OpenJDK 引入了预览特性 结构化并发(Structured Concurrency),属于 Project Loom 的一部分。

结构化并发的目标是将多个并发子任务视为一个整体,统一管理它们的生命周期、错误处理和取消操作,从而提升可靠性和可观测性。

为此,引入了预览 API java.util.concurrent.StructuredTaskScope,用于将一个任务拆分为多个并发子任务,并等待它们完成。

示例代码如下:

Callable<String> task1 = ...
Callable<String> task2 = ...

try (var scope = new StructuredTaskScope<String>()) {

    Subtask<String> subtask1 = scope.fork(task1); // 启动子任务1
    Subtask<String> subtask2 = scope.fork(task2); // 启动子任务2

    scope.join(); // 等待子任务完成

    // 处理子任务结果

}

使用 try-with-resources 语句可以自动管理资源释放。

9. 总结

本文讨论了 Java 当前并发模型存在的问题,以及 Project Loom 提出的解决方案。

我们重点介绍了 OpenJDK 21 引入的 虚拟线程,它为 Java 提供了一种轻量级的并发替代方案,摆脱了对操作系统内核线程的依赖。

如果你正在开发高并发应用,尤其是 I/O 密集型服务,Loom 带来的虚拟线程和结构化并发绝对值得你关注。


原始标题:OpenJDK Project Loom | Baeldung