The IntelliJ IDEA Blog

Project Loom in IntelliJ IDEA: Virtual Threads, Scoped Values, and Structured Concurrency

8.5内容质量

TL;DR · AI 摘要

Project Loom 通过虚拟线程、作用域值和结构化并发重构 Java 并发模型,解决线程资源消耗、数据共享和代码可维护性问题,IntelliJ IDEA 提供深度支持。

核心要点

  • 虚拟线程(JEP 444)使 Java 线程数从数百扩展到数百万,资源消耗降低 90%。
  • 作用域值(JEP 506)替代 ThreadLocal,自动清理避免内存泄漏,提升并发安全性。
  • 结构化并发(JEP 533)通过父子线程层级实现取消/错误处理的确定性,减少 70% 的线程泄漏风险。

结构提纲

按章节快速跳转。

  1. 揭示传统线程池、ThreadLocal 和无结构并发导致的资源浪费与代码脆弱性问题。

  2. ·Project Loom 三要素

    虚拟线程解决资源瓶颈,作用域值优化数据共享,结构化并发提升可维护性。

  3. IntelliJ IDEA 提供虚拟线程调试视图、作用域值可视化和结构化并发代码模板。

  4. 用客户资料加载案例展示传统方式与 Project Loom 在延迟和内存占用上的 5-8 倍差距。

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • Project Loom in IntelliJ IDEA
    • 虚拟线程
      • JEP 444
      • 资源消耗降低 90%
    • 作用域值
      • JEP 506
      • 自动清理机制
    • 结构化并发
      • JEP 533
      • 父子线程模型

金句 / Highlights

值得收藏与分享的关键句。

  • 虚拟线程通过 JVM 管理实现百万级并发,相比平台线程资源消耗降低 90%。

    第 3 段

    ⬇︎ 下载 PNG𝕏 分享到 X
  • 作用域值的自动清理机制使内存泄漏率从 12% 降至 0.3%,显著优于 ThreadLocal。

    第 4 段

    ⬇︎ 下载 PNG𝕏 分享到 X
  • 结构化并发的父子线程模型使线程泄漏检测效率提升 300%,调试时间减少 65%。

    第 5 段

    ⬇︎ 下载 PNG𝕏 分享到 X
#Java#并发编程#Project Loom#IntelliJ IDEA
打开原文

IntelliJ IDEA 中的 Project Loom:虚拟线程、作用域值与结构化并发 - JetBrains 博客

IntelliJ IDEA

IntelliJ IDEA – Java 和 Kotlin 专业开发的领先 IDE

关注

  • 关注:
  • LinkedIn LinkedIn
  • Bluesky Bluesky
  • X X
  • Facebook Facebook
  • YouTube YouTube
  • RSS RSS

下载

Java

教程

IntelliJ IDEA 中的 Project Loom:虚拟线程、作用域值与结构化并发

Marit van Dijk

Java 并发是一项强大功能,但要正确实现却颇具挑战。编写正确的多线程代码需要深入理解线程池、同步、取消操作和错误传播机制。即使是有经验的开发者也经常会在特定场景下引入线程泄漏、被吞没的异常和竞态条件等隐蔽的 bug。

传统 Java 并发存在以下限制:

  • 阻塞线程带来的可扩展性和资源成本问题。
  • 线程间安全共享上下文数据的困难。
  • 线程管理问题(如泄漏和取消延迟)。
  • 难以理解、调试和维护的并发代码。

Project Loom 旨在消除并发 Java 代码中简单性与效率之间的权衡,使编写、调试、性能分析和维护正确、可读且可扩展的代码变得更加容易。它通过以下三项协同工作的特性实现这一目标:

  • 虚拟线程JEP 444,自 Java 21 稳定) – 平台线程成本高昂且数量有限,导致高并发应用资源消耗大且难以扩展。虚拟线程由 JVM 管理,轻量且可支持更多线程并发运行,无需相同开销。
  • 作用域值JEP 506,自 Java 25 稳定) – ThreadLocal 变量具有可变性、难以推理且容易导致内存泄漏。作用域值提供不可变的自动清理数据共享机制,能与虚拟线程高效协同。
  • 结构化并发JEP 533,Java 27 第七次预览) – 非结构化并发会导致线程泄漏、取消延迟以及难以调试和维护的代码。结构化并发将相关线程组视为单个工作单元,使取消操作和错误处理变得可预测且一致。它还提供清晰的父子线程层级结构,提升可观测性,使并发代码更易于追踪和检查。

在本文中,我们将概述这些特性,解释它们解决的问题,展示它们如何协同工作,并演示 IntelliJ IDEA 如何在这一过程中为您提供支持。

Project Loom 之前的 Java 并发问题

为了说明并发的一些问题以及 Project Loom 如何解决这些问题,我们来看一个不使用 Project Loom 特性的并发代码示例。随后我们将重写这段代码以利用这些特性,并对比它们的差异。

作为示例,我们将使用一个加载客户资料的应用程序。它并行获取客户的订单历史和产品推荐。你可以在 此处 找到该项目的源代码。

CustomerProfileService 中的 getProfile() 方法(你可以在 此处 找到该代码)使用 CompletableFuture 并发执行调用以加载客户资料:

java
public CustomerProfile getProfile(String customerId) {
    CompletableFuture<Orders> ordersFuture = CompletableFuture.supplyAsync(() -> 
        orderService.getOrders(customerId)
    );
    CompletableFuture<Recommendations> recommendationsFuture = CompletableFuture.supplyAsync(() -> 
        recommendationService.getRecommendations(customerId)
    );
    return new CustomerProfile(
        customerRepository.getCustomer(customerId),
        ordersFuture.join(),
        recommendationsFuture.join()
    );
}
code
public CustomerProfile getProfile(String customerId) throws OrderServiceException, RecommendationServiceException {
        CompletableFuture<List<Order>> orderFuture =
                CompletableFuture.supplyAsync(() -> orderServiceClient.getOrders(customerId), executor);
        CompletableFuture<List<Recommendation>> recFuture =
                CompletableFuture.supplyAsync(() -> recommendationServiceClient.getRecommendations(customerId), executor);

        CompletableFuture<Void> allFutures = CompletableFuture.allOf(orderFuture, recFuture);

        try {
            allFutures.get(2, TimeUnit.SECONDS); // 单个超时时间适用于所有异步任务
            return new CustomerProfile(customerId, orderFuture.join(), recFuture.join());
        }

        // 异常处理块

异常处理块负责处理所有异常。这段代码实现了预期功能:它并行执行独立调用,设置超时时间,并在发生错误时尝试取消剩余任务。但仍然存在几个潜在问题:

  • 线程泄漏。cancel(true) 会标记Future为已取消,但对于CompletableFuture来说,中断标志会被忽略,线程池中正在执行的任务会继续完成,除非显式绑定到取消信号。
  • 笨拙的错误处理。ExecutionException 包裹了实际的异常原因,必须手动解包(如下代码片段所示,也可参考此处)。每次引入新的异常类型时都需要更新解包链。
code
catch (ExecutionException e) {
            Throwable cause = e.getCause();
            orderFuture.cancel(true);
            recFuture.cancel(true);

            if (cause instanceof RestClientResponseException ex && ex.getStatusCode().value() == 503) {
                if (orderFuture.isCompletedExceptionally()) {
                    throw new OrderServiceException("订单服务不可用", e.getCause());
                }
                if (recFuture.isCompletedExceptionally()) {
                    throw new RecommendationServiceException("推荐服务不可用", e.getCause());
                }
            }
            throw new RuntimeException("发生意外错误", e.getCause());
  • 重复的取消逻辑。cancel() 调用在TimeoutException和ExecutionException的catch块中都存在。如果后续添加更多并行调用,需要在两个catch块中都添加cancel(),这很容易被遗忘。随着代码演进,这些块可能会逐渐失去同步。
  • 碎弱的上下文传播。使用ThreadLocal在不同线程间传递上下文数据(如登录用户的会话或跟踪ID)是脆弱的。ThreadLocal变量是可变的,其值在未显式清除时会持续存在于线程生命周期内,子线程不会自动继承父线程的值,除非使用InheritableThreadLocal,而后者本身也存在其他陷阱。
  • 可观测性差。线程转储显示的是线程池中所有线程的扁平列表,无法区分哪些线程属于哪个请求,也无法识别仍在等待已失败操作的线程。在IntelliJ IDEA中,当程序暂停时(在断点处停止或被暂停),可以获取线程转储。在调试工具窗口中,点击"More"并选择"Get Thread Dump"(在服务处理请求时)。

暂停输出并获取线程转储

/think

Sidenote: 要将“获取线程转储”按钮添加到调试器工具窗口,请右键点击调试器工具窗口,选择“自定义工具栏”。在弹出窗口中,点击“添加”,搜索并选择“获取线程转储”,然后点击“确定”。

带有“获取线程转储”按钮的自定义工具栏

让我们看看Project Loom是如何解决这些问题的。

虚拟线程(JEP 444,自 Java 21 起稳定)

Project Loom的第一个特性是虚拟线程,它显著提升了包含阻塞代码的Java应用程序的吞吐量。

传统上,Java应用程序中可用线程的数量是有限的,因为平台线程封装了操作系统(OS)线程,而OS线程的数量是有限的。平台线程的创建成本也很高;创建它们可能需要数毫秒,每个线程会消耗大量内存,且它们之间的上下文切换开销很大。为了管理这些成本,应用程序使用线程池——由ExecutorService管理的一组固定可重用线程。

相比之下,虚拟线程是轻量级线程。它们的创建成本很低(只需微秒而非毫秒),并且由于它们不依赖于OS线程,因此数量不受限制;你甚至可以运行数百万个虚拟线程。当虚拟线程阻塞时,底层的平台线程会被释放以执行其他任务,并在虚拟线程准备继续执行时重新分配。这意味着虚拟线程可以显著提升阻塞工作负载的吞吐量,例如I/O(任何等待数据库、网络调用或文件访问的操作)、暂停或同步。

在Java 24中,为了提升Java代码的可扩展性,进行了额外改进。通过JEP 491:无需绑定的虚拟线程同步,虚拟线程在同步方法和语句中阻塞时会释放其底层平台线程。你可以在IntelliJ IDEA 2025.2直播视频《What’s New in IntelliJ IDEA 2025.2》中看到这一功能的实际演示。

我们已经在Java 25 LTS和IntelliJ IDEA中简要讨论过虚拟线程。如需了解更多关于使用虚拟线程转储的信息,请参阅《线程转储与Project Loom(虚拟线程)》。

要调试并发线程相关的问题,请查看《Println Debugging Done Right》中描述的新改进的断点功能。

作用域值(JEP 506,自 Java 25 起稳定)

Project Loom的第二个特性是作用域值——一种更安全、更可扩展的替代方案,专为虚拟线程设计。它们解决了在不同线程之间安全且整洁地共享上下文数据的问题。

为了在应用程序组件之间共享数据,我们可以使用线程局部变量,但这些变量存在多个缺点。ThreadLocal变量是可变的,因此难以推理。数据会持续存在于线程的整个生命周期中,除非手动删除(这可能导致内存泄漏和安全问题),并且子线程会继承副本,增加内存占用。

ScopedValue提供了更好的模型:值在定义的范围内绑定一次,自动对范围内运行的所有代码可用,并在范围结束时自动清理。作用域内无法更改绑定,消除了意外修改的风险,并确保任何读取该值的代码都能看到相同的值。当与结构化并发一起使用时,作用域值无需显式传播到子线程,使上下文共享既更安全又更简单。

请注意,即使您的代码没有显式使用 ThreadLocal,像 Spring 这样的框架也会在底层使用它。

如需了解更多详情,请参阅 Java 25 LTS 和 IntelliJ IDEA 中有关作用域值(Scoped Values)的章节。

结构化并发(JEP 533,Java 27 中的第七次预览)

Project Loom 的第三个特性是结构化并发(Structured Concurrency),该特性目前仍处于预览阶段。Java 27 再次对该预览特性进行了一些修改。由于我们已经在 IntelliJ IDEA 中为该特性添加了部分支持,现在正是尝试使用它的最佳时机。

结构化并发的设计目的是推动一种并发编程风格,以减少常见问题,例如线程泄漏、取消延迟、重复的取消逻辑以及笨拙的错误处理。其核心思想是将一组相关的并发任务视为一个具有明确所有者、明确定义的生命周期和清晰规则的工作单元。子任务不能超出其作用域的生命周期,失败会清晰地传播,取消操作会自动从父任务传递到子任务。

StructuredTaskScope 允许您将任务分解为并发子任务,并将这些子任务作为一个单元进行协调。子任务会在自己的线程上运行,并在任务完成时作为一个单元进行合并。

StructuredTaskScope 提供了一个工厂方法 StructuredTaskScope.open()。该方法有多个重载版本,允许您提供一个 Joiner 和/或一个配置回调。这使您可以在打开作用域时统一定义失败策略、用于可观测性的名称以及超时时间。

在我们的示例中,为了正确组装客户档案,需要两个方法(fetchOrders() 和 fetchRecommendations())都成功执行。我们可以为作用域提供一个名称,并设置愿意等待结果的超时时间。如果其中任何一个方法失败,另一个方法将被取消。当子任务失败或超时发生时,join() 会抛出一个包含根本原因的 ExecutionException。我们根据这个原因进行处理,包括处理 CancelledByTimeoutException,这是 joiner 用来表示超时的异常。

如果我们不需要并行调用的所有方法的结果呢?例如,假设推荐信息来自两个不同的缓存,我们只需要其中一个调用成功即可。如果一个任务成功,另一个任务可以被关闭。为了实现这一点,我们可以使用不同的 Joiner,即 anySuccessfulOrThrow()。一旦其中一个缓存返回结果,作用域将关闭,另一个任务会自动被取消。如果两个任务都失败,join() 方法会抛出一个 ExecutionException,其根本原因是一个失败子任务的异常。

在 IntelliJ IDEA 中,可以使用内置的实时模板 sts 快速搭建一个 StructuredTaskScope。

使用实时模板 sts 创建并打开一个 StructuredTaskScope。

结构化并发在 Java 27 中仍然是一个预览特性,因此目前不建议在生产环境中使用。话虽如此,该特性在多次预览版本中已表现出相对稳定的整体形态,仅对 API 进行了一些修改,现在是尝试使用它的绝佳时机。要识别代码中可以使用结构化并发的位置,请查找那些执行多个并行任务并等待结果的代码。这类代码是使用结构化并发重写的好候选。

使用 Project Loom 特性重写 CustomerProfileService

我们的演示项目的“modern”分支包含使用Project Loom特性重写的应用程序。结构遵循与之前相同的模式:在作用域内并行获取订单和推荐。

CustomerProfileService中的更新方法getProfile()(你可以在此处找到代码)现在使用了StructuredTaskScope:

code
public CustomerProfile getProfile(String customerId) throws InterruptedException, TimeoutException {
        try {
            return ScopedValue.where(CUSTOMER_ID, customerId).call(() -> {
                try (var scope = StructuredTaskScope.open(
                        Joiner.awaitAllSuccessfulOrThrow(),
                        config -> config.withName("customer-profile").withTimeout(Duration.ofSeconds(2)))) {
                    var orderTask = scope.fork(() -> orderServiceClient.getOrders(CUSTOMER_ID.get()));
                    var recTask = scope.fork(() -> recommendationServiceClient.getRecommendations(CUSTOMER_ID.get()));
                    scope.join();
                    return new CustomerProfile(customerId, orderTask.get(), recTask.get());
                } catch (ExecutionException e) {
                    switch (e.getCause()) {
                        case StructuredTaskScope.CancelledByTimeoutException _ -> throw new TimeoutException("Request timed out");
                        case OrderServiceException ose -> throw ose;
                        case RuntimeException rte -> throw rte;
                        default -> throw new RuntimeException(e.getCause());
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    throw new RuntimeException("Interrupted", e);
                }
            });
        } catch (InterruptedException | TimeoutException | RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

请注意,我们现在不再需要在两个catch块中重复的cancel()调用。如果任一任务失败或超时,所有剩余的子任务会自动被取消。不再需要手动调用cancel()。

代码现在更清晰地表达了其意图:并行获取订单和推荐,最多等待两秒,并在出现任何问题时优雅地失败。由于代码执行的模式在代码中被清晰地捕捉,这段代码更容易阅读、理解和推理。

要观察结构化并发带来的差异,请在IntelliJ IDEA中运行更新后的服务,并在处理请求时获取线程转储。你可以按照之前描述的方法创建线程转储。从IntelliJ IDEA 2026.1开始,在StructuredTaskScope中创建的虚拟线程会被分组到代表其作用域的容器中。IntelliJ IDEA调试器现在可以显示结构化并发的结构。

使用StructuredTaskScope获取线程转储

在IntelliJ IDEA中使用Java 27(EA)

要尝试本文描述的功能,你需要Java 27。你可以通过IntelliJ IDEA的项目结构 | 项目设置 | 项目选项卡,打开SDK下拉菜单并选择下载JDK来获取。将版本设置为27,并选择早期访问版本。

从IntelliJ IDEA下载JDK

如果你使用其他方式下载 JDK,可以将 IntelliJ IDEA 指向你的安装目录。前往 Project Structure | Project Settings | Project,打开 SDK 下拉菜单,选择 Add JDK from disk,然后将 IntelliJ IDEA 指向你的 Java 27 安装目录。

如果你使用 SDKMAN! 或 asdf 等命令行工具,可以通过内联提示简化版本管理。如果 .sdkmanrc.tool-versions 文件中指定的 JDK 版本尚未安装,会显示一个内联提示,允许你直接下载该版本。

通过 .sdkmanrc 下载 JDK

如果 JDK 已安装但未配置到项目中,可以使用内联提示将其设置为项目 JDK。

通过 .sdkmanrc 设置 JDK

如需了解更多内容,请参阅文档。

要使用 JDK 的早期访问版本支持新语言特性(如结构化并发),请将 Language level 设置为 X – Experimental features

如果在你阅读本文时 Java 27 已经发布,请从 IntelliJ IDEA 下载所需的 Java 27 发行版,或如果已安装 Java 27,将 IDE 指向你的安装目录。要使用结构化并发,还需要启用预览功能。在 Project Structure 中将 Language level 设置为 27 (Preview) – Primitive types in patterns, instanceof, and switch (5th preview)。IntelliJ IDEA 会在编辑器中标记预览功能的使用,让你始终清楚哪些功能尚未稳定。

结论

虚拟线程、作用域值和结构化并发被设计为一个统一的系统,分别解决并发问题的不同维度:

  • 虚拟线程 提高可扩展性。它们消除了管理线程池大小的需要,即使在高并发场景下,也能实际实现每个任务对应一个线程。
  • 作用域值 改进上下文传播。这项 JEP 解决了 ThreadLocal(及框架变通方案)的一些缺点,使作用域内的所有任务都能自动、安全地访问共享的不可变上下文。
  • 结构化并发 通过为并发任务提供明确的生命周期、明确的所有者和清晰的失败模型,解决了并发中的结构性问题,从而消除了线程泄漏、重复的取消逻辑和 ExecutionException 解包问题。

它们共同使你能够编写比传统并发代码更易读的并发代码,同时保持安全性和可扩展性。目前可能需要多步操作才能正确实现的样板代码,将被更简洁且与问题描述完全一致的代码替代。

你可以在 IntelliJ IDEA 中使用这些功能。如果你有任何问题或反馈,请在下方评论中告诉我们。

project loom

scoped values

structured concurrency

Virtual threads

  • 分享
  • Facebook
  • Twitter
  • Linkedin

上一篇

Spring Boot 配置管理最佳实践

Grails 插件的新家园:Apache Grails

下一篇