编程

OpenJDK 的 Loom 项目

612 2024-07-06 15:58:00

1. 概述

本文中,我们将快速了解 Loom 项目。从本质上讲,Loom 项目的主要目标是支持 Java 中的高吞吐量、轻量级并发模型。

2. Loom 项目

Loom 项目是 OpenJDK 社区为 Java 引入轻量级并发结构的一次尝试。到目前为止,Loom 的原型已经在 JVM 和 Java 库中引入了一次更改。

虽然 Loom 还没有计划发布,但我们可以在 Loom 项目的 wiki 上访问最新的原型。

在我们讨论 Loom 的各种概念之前,让我们讨论一下 Java 中当前的并发模型。

3. Java 的并发模型

目前,线程(Thead)代表了 Java 中并发的核心抽象。这种抽象以及其他并发 API 使编写并发应用变得容易。

然而,由于 Java 使用操作系统内核线程来实现,因此无法满足当今的并发性要求。特别存在两个主要问题:

  1. 线程无法与域的并发单元的规模相匹配。例如,应用通常允许多达数百万的事务、用户或会话。然而,内核支持的线程数量要少得多。因此,为每个用户、事务或会话指定一个线程通常是不可行的 
  2. 大多数并发应用需要在每个请求的线程之间进行一些同步。因此,在操作系统线程之间会发生代价高昂的上下文切换。

此类问题的一个可能解决方案是使用异步并发 API。常见的例子有 CompletableFutureRxJava。只要这样的 API 不阻塞内核线程,它就会在 Java 线程之上为应用提供更细粒度的并发结构。

另一方面,这样的 API 更难调试,也更难与旧版 API 集成。因此,需要一个独立于内核线程的轻量级并发结构。

4. 任务和调度器

线程的任何实现,无论是轻量级还是重量级,都依赖于两个构造:

  1. 任务 (也称为 continuation) – 一系列指令,可以暂停执行某些阻塞操作
  2. 调度器(Scheduler) – 用于将 continuation 分配给 CPU,并从暂停的 continuation 重新分配 CPU

目前,Java 依赖于操作系统实现来进行 continuation 和调度器(scheduler)。

现在,为了暂停 continuation,需要存储整个调用栈。同样,在恢复时检索调用堆栈。由于 continuations 的操作系统实现包括本机调用堆栈和 Java 的调用堆栈,因此占用空间很大。

然而,更大的问题是操作系统调度器的使用。由于调度器在内核模式下运行,因此线程之间没有区别。并且它以相同的方式处理每个 CPU 请求。

这种类型的调度对于 Java 应用程序尤其不是最优的

例如,想象一个应用程序线程,它对请求执行一些操作,然后将数据传递给另一个线程进行进一步处理。在这里,最好将这两个线程安排在同一个 CPU 上。但是,由于调度器与请求 CPU 的线程无关,因此无法保证这一点。

Loom 项目建议通过用户模式线程来解决这一问题,该线程依赖于 Java 运行时的 continuation 和调度器实现,而不是 OS 实现。

5. 纤程(Fiber)

在 OpenJDK 最近的原型中,一个名为 Fiber 的新类与 Thread 类一起被引入到库中。

由于 Fiber 的计划库与 Thread 相似,因此用户实现也应保持相似。然而,有两个主要区别:

  1. Fiber 在内部用户模式 continuation 中包装任何任务。这将允许任务在  Java 运行时而不是内核中暂停和恢复
  2. 使用一个可插入的用户模式调度器 (如,ForkJoinPool)

让我们详细地看一下这两项。

6. Continuation

continuation (或协程 co-routine) 是一系列指令,调用方可以在稍后阶段生成并恢复这些指令。

每一个 continuation 都有一个入口点和一个 yield 点。yield 点是它被暂停的地方。每当调用方恢复 continuation 时,控制都会返回到最后一个 yield 点。

重要的是要意识到,这种暂停/恢复现在发生在语言运行时中,而不是操作系统中。

因此,它防止了内核线程之间昂贵的上下文切换。

与线程类似,Loom 项目旨在支持嵌套纤程。由于纤程内部依赖于 continuation ,因此它也必须支持嵌套的 continuation 。要更好地理解这一点,假设由一个允许嵌套的 Continuation 类:

Continuation cont1 = new Continuation(() -> {
    Continuation cont2 = new Continuation(() -> {
        //do something
        suspend(SCOPE_CONT_2);
        suspend(SCOPE_CONT_1);
    });
});

如上所示,此嵌套的 continuation 可以通过传递作用域变量暂停自己或者任何附入的 continuation。出于这个原因,它们被称为作用域 continuation。

由于暂停 continuation 还需要它存储调用栈,因此 Loom 项目也有一个目标,即在恢复 continuation 的同时添加轻量级堆栈检索。

7. 调度器(Scheduler)

早些时候,我们讨论了操作系统调度器在同一 CPU 上调度相关线程时的缺点。

尽管 Loom 项目的目标是允许使用 Fiber 的可插入调度器,但异步模式下, ForkJoinPool 将用作默认调度器。

ForkJoinPool 使用工作窃取算法。因此,每个线程都维护一个任务 deque,并从头开始执行任务。此外,任何空闲线程都不会阻塞或者等待任务,而是从另一个线程的 deque 的尾部提取任务

异步模式中唯一的区别是工作线程从另一个 deque 的头上窃取任务

ForkJoinPool 将由另一个正在运行的任务调度的任务添加到本地队列中。因此,在同一 CPU 上执行它

8. 结论

本文中,我们讨论了 Java 当前并发模型中的问题以及 Loom 项目中提议的更改。

在这样做的过程中,我们还定义了任务和调度器,并研究了 Fiber 和 ForkJoinPool 如何使用内核线程提供 Java 的替代方案。