Zademy

Java 中的虚拟线程:可扩展性与复杂性平衡

Java; 虚拟线程; ProjectLoom; 并发; JDK25
973 字

传统并发的问题

Java 应用程序传统上依赖 平台线程(Platform Threads 或传统线程)进行并发。

平台线程(PT):

  • 作为操作系统管理线程的包装器,具有 1
    映射。
  • 昂贵且重量级,每个线程消耗约 1-2 MB 内存和操作系统资源。
  • 受系统资源限制。创建过多会导致 OutOfMemoryError 或使上下文切换饱和。
  • 在 Web 应用中,"每个请求一个线程"(Thread-per-Request)模型直观但无法很好地扩展高并发(例如 >10k 并发请求)。

需求: 在微服务、流处理或交易 API 等高并发场景中,需要低延迟和高可扩展性。响应式编程(Project Reactor、RxJava)等解决方案提高了吞吐量,但使代码和调试复杂化。

什么是虚拟线程(VT)?

Project Loom 引入并在 JDK 21(JEP 444)中稳定化的虚拟线程是 Java 解决大规模并发而不使用异步编程的方案。

主要特性:

  • 轻量级线程: 由 JVM 管理的 java.lang.Thread 实例(约 1 KB 每线程)。
  • 数百万线程: 允许创建 数百万 线程而不耗尽内存。
  • I/O 绑定可扩展性: 针对阻塞任务(非 CPU 绑定)的吞吐量优化。
  • 完全兼容: 现有 Thread 代码的即插即用替代品。类似于 Go 的 goroutines
  • JDK 21+ 可用: 自 JDK 21 稳定;JDK 22-25 持续改进。

内部工作原理:M
调度

M

映射: M 个虚拟线程(数百万)映射到 N 个平台线程(CPU 核心)。

  1. 载体: 承载虚拟线程的平台线程。
  2. 挂载/卸载: 虚拟线程挂载到载体上执行;在 I/O 阻塞时(sleep()、套接字、数据库查询)卸载。
  3. JVM 调度器: 动态重新分配载体。载体在 I/O 等待期间保持空闲。
  4. 延续: I/O 完成后,虚拟线程返回调度器并在任何可用载体上重新挂载。

这最大化了 CPU 利用率:载体始终忙碌。

最新更新:

  • JDK 24(JEP 491): 消除了 synchronized 块中的 固定(虚拟线程正确卸载)。
  • JDK 25(JEP 506): 作用域值作为 ThreadLocal 的不可变替代品,防止虚拟线程中的内存泄漏。

实际示例

针对 I/O 绑定(HTTP、数据库、文件)优化。

示例 1:简单虚拟线程

Thread.startVirtualThread(() -> {
    System.out.println("来自 VT 的问候!" + Thread.currentThread());
});

示例 2:ExecutorService(高并发)

import java.util.concurrent.Executors;
import java.time.Duration;

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    // 10k 并发任务
    IntStream.range(0, 10_000).forEach(i ->
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1)); // 模拟 I/O
            // 业务逻辑
        })
    );
} // 自动等待完成

示例 3:Spring Boot + VT(真实基准测试)

在最近的基准测试中(JDK 24 + Spring Boot + Postgres),虚拟线程在 10k req/s 时将延迟降低约 50%,相比平台线程。

@RestController
public class ApiController {
    @GetMapping("/data")
    String getData() {
        // VT 自动处理请求(Tomcat/Undertow 配置)
        return fetchFromDb(); // 阻塞但可扩展
    }
}

高级考虑和最佳实践

CPU 绑定 vs I/O 绑定

类型推荐原因
I/O 绑定(数据库、HTTP)VT + newVirtualThreadPerTaskExecutor()大规模可扩展性
CPU 绑定(加密、机器学习)平台线程池 = CPU 核心数避免 载体饥饿

问题和解决方案

  • 固定: JDK 24 中 synchronized 已解决。避免 JNI/原生代码。
  • ThreadLocal: ❌ 与 VT(内存爆炸)。使用 作用域值(JDK 25 预览版)。
  • 指标/监控: Thread.currentThread().isVirtual();JFR 自 JDK 21 支持 VT。
  • 框架: Spring Boot 3.2+、Quarkus、Helidon 原生。

最佳实践(2025):

  1. 在 Web 服务器中默认使用 VT。
  2. 使用 JDK Mission Control/JFR 分析以检测固定。
  3. ThreadLocal 迁移到作用域值。
  4. CPU 密集型使用固定池。
  5. 使用 wrk/ab 测试 >100k 并发。

最终总结

档次使用 VT?
初学者是,Thread.startVirtualThread() 用于所有并发。
专家I/O 使用是;纯 CPU 使用否。监控固定。

比喻: PT = 每人专用电梯(昂贵)。VT = 气动管道系统:数千个轻量胶囊在少数物理管道中。

来源:OpenJDK JEPs、Spring Boot 基准测试(Reddit/Java 24)、RockTheJVM/RabinaNPatra 指南(2025)。