Java 虚拟线程 (Project Loom):高并发轻量线程

FreeGuideOnline 最新 2026-06-17

Java 虚拟线程完整指南 (Project Loom)

什么是虚拟线程?

虚拟线程是 Java 平台在 Project Loom 中引入的轻量级线程实现。它们由 JVM 管理,而不是由操作系统内核调度。与传统的平台线程(操作系统线程)不同,虚拟线程的创建和切换成本极低,使得开发者可以轻松创建数百万个并发任务,而不必担心耗尽系统资源或复杂的线程池管理。

虚拟线程的核心思想是:用廉价的“用户态线程”承载大量阻塞操作,当虚拟线程遇到 I/O、睡眠等阻塞操作时,JVM 会自动将其从底层载体线程(平台线程)上“卸下”,让出 CPU 去执行其他虚拟线程,从而保持高吞吐量。

为什么需要虚拟线程?

传统 Java 并发模型面临两大瓶颈:

  1. 平台线程昂贵且有限:每个 java.lang.Thread 实例都直接映射到一个操作系统线程。操作系统线程数量受限于内存和内核调度能力,通常一台服务器最多只能创建几千个线程。在“一个请求对应一个线程”的编程模型下,处理海量并发请求时很容易达到上限。
  2. 异步编程复杂难维护:为突破线程限制,开发者被迫使用回调、 CompletableFuture、反应式框架等异步模型。虽然解决了资源问题,但代码变得难以阅读、调试和追踪,也容易产生“回调地狱”。

虚拟线程完美融合了同步编程的简单性异步执行的高吞吐。你可以用完全同步、阻塞的方式编写代码(就像写单线程程序一样),而 JVM 会负责将这些阻塞调用转化为非阻塞调度,从而使极少数的平台线程支撑起海量虚拟线程。

基础用法

创建一个虚拟线程

在你的项目中,只需使用 Java 21 或更高版本(虚拟线程在 Java 19 中作为预览特性引入,21 正式发布),即可使用以下 API。

// 方法1:通过 Thread 类的静态工厂方法
Thread vThread = Thread.ofVirtual()
        .name("my-virtual-thread")
        .start(() -> {
            System.out.println("运行在虚拟线程中");
        });

// 等待该线程执行完毕
vThread.join();

也可以直接创建但不启动,然后手动 start()

Thread vThread = Thread.ofVirtual()
        .name("worker")
        .unstarted(() -> {
            // 线程逻辑
        });
vThread.start();

使用 ExecutorService 管理虚拟线程

推荐通过 Executors.newVirtualThreadPerTaskExecutor() 获得一个为每个任务分配新虚拟线程的执行器。这个执行器的行为符合传统线程池的语义,但内部实现极为轻量,不限制虚拟线程数量。

try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 10_000; i++) {
        int taskId = i;
        executor.submit(() -> {
            // 模拟阻塞操作(比如网络请求)
            try {
                Thread.sleep(1000);
                System.out.println("任务 " + taskId + " 完成");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
    }
} // executor 关闭时会等待所有任务完成

使用 try-with-resources 确保所有任务提交完毕并等待完成后再释放资源。

虚拟线程与平台线程的映射

虚拟线程在运行时由一组固定的载体线程 (Carrier Thread) 承载。载体线程就是普通平台线程,通常数量等于 CPU 核心数。当虚拟线程执行阻塞操作(如 Thread.sleepSocket.readLockSupport.park)时,虚拟线程会从载体线程上被卸载(unmount),载体线程立即去执行另一个就绪的虚拟线程。阻塞操作完成后,虚拟线程会被重新挂载(mount)到可用的载体线程上继续执行。

开发者无需直接管理载体线程,JVM 会自动完成调度。

深入理解:虚拟线程的阻塞处理

虚拟线程的神奇之处在于让几乎所有阻塞 API 都变为可感知虚拟线程的阻塞。以下常见操作在虚拟线程中都不会物理阻塞载体线程:

  • Thread.sleep()
  • Object.wait() / Condition.await()
  • LockSupport.park()
  • Socket 网络 I/O(包括 SocketServerSocketDatagramSocket
  • File I/O (Java 21 中部分文件操作仍会占用载体线程,Java 22+ 逐步优化)
  • Process 相关操作

因此你可以用最直观的同步代码实现网络服务,例如:

ServerSocket server = new ServerSocket(8080);
while (true) {
    Socket socket = server.accept(); // 在虚拟线程中会卸载
    Thread.ofVirtual().start(() -> {
        handleRequest(socket);
    });
}

线程局部变量与 ThreadLocal

虚拟线程支持 ThreadLocal,但需要谨慎使用。因为虚拟线程数量可能非常多,如果每个虚拟线程都在 ThreadLocal 中存储大量对象,容易导致内存占用过高。官方建议:可以使用,但不要存储大型数据,或考虑使用作用域值(Scoped Values)替代。

并发限制:不要直接创建无限虚拟线程

虽然虚拟线程本身廉价,但你仍然需要控制并发资源的占用量(如数据库连接数、外部 API 调用频率)。可以使用 Semaphore 对虚拟线程进行并发控制:

Semaphore semaphore = new Semaphore(10);
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 1000; i++) {
        executor.submit(() -> {
            try {
                semaphore.acquire();
                // 调用有限资源的外部服务
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                semaphore.release();
            }
        });
    }
}

结构化并发 (Structured Concurrency)

Project Loom 不仅带来虚拟线程,还引入了结构化并发(仍为预览特性,需使用 --enable-preview)。它的核心思想是将多个并发任务视为一个工作单元,任务的生命周期必须被显式管理,防止线程泄露和遗弃。

基本用法:

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<String> result1 = scope.fork(() -> fetchUser());
    Future<Integer> result2 = scope.fork(() -> fetchOrderCount());
    
    scope.join();           // 等待所有子任务完成
    scope.throwIfFailed();  // 如果任一任务失败,抛出异常
    
    String user = result1.resultNow();
    int orders = result2.resultNow();
    System.out.println(user + " 有 " + orders + " 个订单");
}

结构化并发要求通过 StructuredTaskScope 创建子任务,当 try 块结束时,scope 会确保所有未完成的子任务都被取消。这消除了忘记关闭线程池或无限等待的风险。

结构化并发的好处

  • 清晰的所有权:创建任务的代码块同时也是等待和取消任务的地方。
  • 异常传播:子任务失败可优雅地取消其他同组任务并向上传播异常。
  • 可观察性:线程 dump 可以展示父子任务关系,方便调试。

注意:结构化并发目前仍是预览 API,生产环境使用需谨慎,并需添加 --enable-preview 编译和运行时标志。

虚拟线程的局限性

  • 不适用于 CPU 密集型任务:如果任务长时间占用 CPU(例如复杂计算),虚拟线程被挂载到载体线程上后也会一直占用该载体线程。此时不可能被自动卸载,从而影响其他虚拟线程的执行。对这种场景,仍然推荐使用有限数量的平台线程或 ForkJoinPool 来避免调度开销。
  • Native 代码或长时间持有的监视器:当虚拟线程进入 synchronized 块或执行 JNI native 方法时,JVM 无法将其从载体线程上卸载。如果长时间持有监视器,会导致载体线程被 pinned(钉住),影响整体吞吐量。Java 21 中已优化大部分 synchronized 相关卸载问题,但仍有少数情况无法卸载。建议在高并发场景下使用 java.util.concurrent 锁(如 ReentrantLock)替代 synchronized
  • 线程 dump 更庞大:由于可能存在海量虚拟线程,传统的线程 dump 会非常长。JDK 提供了新的线程 dump 格式(JSON、轻量级模式),以便过滤和分析。
  • 调试工具的适配:IDE 和 profiler 需要适配虚拟线程的展示方式,否则可能看不到虚拟线程或表现异常。

迁移建议与最佳实践

  1. 直接替换线程池方案:如果你的应用使用“一个任务一个线程”的模型(如 Servlet 容器),可以尝试将 ExecutorService 替换为 Executors.newVirtualThreadPerTaskExecutor()。像 Spring Boot 3.2+ 已经内置对虚拟线程的支持,只需简单配置即可启用。
  2. 重构异步代码:逐步将回调式或响应式代码还原为同步、顺序的写法,利用虚拟线程保持吞吐量。
  3. 控制资源并发:无论虚拟线程多轻量,连接池、限流器仍必不可少。使用 Semaphore 或专用的限流库保护后端资源。
  4. 监控与调优:关注 JFR (JDK Flight Recorder) 中的虚拟线程事件,监控 pinned 线程发生的频率,逐步优化监视器使用。
  5. 预览结构化并发:在设计新服务时,尝试使用结构化并发编写更健壮的并发逻辑。

快速试验

你的项目只要配置 Java 21,即可立即体验虚拟线程。例如,编写一个简单的 HTTP 服务器:

import java.net.*;
import java.io.*;

public class VirtualServer {
    public static void main(String[] args) throws Exception {
        try (ServerSocket server = new ServerSocket(9999)) {
            System.out.println("虚拟线程服务器启动,端口 9999");
            while (true) {
                Socket client = server.accept();
                Thread.ofVirtual().start(() -> {
                    try (client;
                         BufferedReader in = new BufferedReader(
                             new InputStreamReader(client.getInputStream()));
                         PrintWriter out = new PrintWriter(client.getOutputStream(), true)) {
                        String line;
                        while ((line = in.readLine()) != null) {
                            out.println("Echo: " + line);
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                });
            }
        }
    }
}

这个服务器能轻松处理数万个并发连接,无需任何线程池配置。编译运行后,用工具测试并发,你会看到惊人的吞吐量。


总结

Project Loom 的虚拟线程彻底改变了 Java 的并发编程模型。你不再需要在同步/简单和异步/高效之间做出取舍。虚拟线程让“为每个任务分配一个线程”重新成为最佳实践,极大地简化了代码,同时保持了 JVM 在高并发场景下的领先性能。搭配结构化并发,Java 正在构建一套更安全、更易维护的并发基础设施。

建议从 Java 21 开始,在新的项目中优先考虑虚拟线程,并逐步改造遗留代码,享受同步编程的纯粹快乐与高并发的强大力量。