Java 21 重磅新特性:虚拟线程(Virtual Threads)实战指南

一、引言

2023年9月,Oracle正式发布了 Java 21,这是继 Java 17 之后的又一个长期支持(LTS)版本。在众多新特性中,虚拟线程(Virtual Threads) 无疑是备受关注的焦点。它由 OpenJDK 的 Project Loom 孵化而来,旨在从根本上简化 Java 高并发编程的复杂性,让开发者能够以更低的成本构建高吞吐量的服务器应用。

传统的 Java 线程(平台线程)是操作系统内核线程的轻量级封装,创建成本高、上下文切换开销大,导致在高并发场景下,我们不得不使用线程池等复杂手段来控制线程数量。虚拟线程的出现,彻底改变了这一局面。

二、什么是虚拟线程?

虚拟线程是 java.lang.Thread 的一种新实现,它是一种用户态线程。与传统的平台线程(Platform Thread,即操作系统内核线程)不同,虚拟线程由 JVM 管理,而不是由操作系统管理。

核心概念:

  • 平台线程(Platform Thread): 操作系统内核线程的包装,属于 heavyweight 资源。
  • 虚拟线程(Virtual Thread): JVM 管理的轻量级线程,属于 lightweight 资源。
  • Carrier 线程: 真正执行任务的平台线程。虚拟线程会挂载(Mount)到 Carrier 线程上运行,遇到阻塞操作时会被卸载(Unmount),从而释放 Carrier 线程去执行其他虚拟线程。

三、为什么需要虚拟线程?

在传统 IO 密集型应用中,一个请求通常对应一个线程。当线程进行 IO 操作(如数据库查询、RPC 调用、文件读写)时,线程大部分时间处于阻塞等待状态。由于平台线程是稀缺资源,我们不得不使用线程池来限制并发数,这导致了:

  1. 线程利用率低: 线程在等待 IO 时被浪费。
  2. 编程模型复杂: 需要引入异步编程、回调、Reactive 等复杂模型。
  3. 系统吞吐量受限: 线程数量受限于操作系统资源。

虚拟线程解决了这些问题:百万级虚拟线程可以轻松创建,且阻塞开销极低,让同步编程模型重新成为首选。

四、快速上手:创建虚拟线程

Java 21 提供了多种创建虚拟线程的方式。

4.1 方式一:Thread.startVirtualThread()

public class VirtualThreadDemo {
    public static void main(String[] args) {
        // 创建并启动一个虚拟线程
        Thread vt = Thread.startVirtualThread(() -> {
            System.out.println("Hello from Virtual Thread: " + Thread.currentThread());
        });

        try {
            vt.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

4.2 方式二:Thread.ofVirtual()

public class VirtualThreadDemo2 {
    public static void main(String[] args) {
        // 创建虚拟线程但暂不启动
        Thread vt = Thread.ofVirtual()
                .name("my-virtual-thread")
                .unstarted(() -> {
                    System.out.println("Running in: " + Thread.currentThread().getName());
                });
        
        vt.start();
        
        try {
            vt.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

4.3 方式三:Executors.newVirtualThreadPerTaskExecutor()

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class VirtualThreadExecutorDemo {
    public static void main(String[] args) {
        // 每个任务创建一个新的虚拟线程
        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 10; i++) {
                int taskId = i;
                executor.submit(() -> {
                    System.out.println("Task " + taskId + " running on " + Thread.currentThread());
                });
            }
        }
        // try-with-resources 会自动关闭 ExecutorService
    }
}

五、实战对比:虚拟线程 vs 平台线程

为了直观感受虚拟线程的强大,我们做一个简单的压力测试:创建大量线程并执行简单的 IO 模拟操作。

5.1 平台线程示例(传统方式)

import java.util.concurrent.CountDownLatch;

public class PlatformThreadTest {
    public static void main(String[] args) throws InterruptedException {
        int threadCount = 10_000;  // 试试 10 万个?可能会 OOM
        CountDownLatch latch = new CountDownLatch(threadCount);
        long start = System.currentTimeMillis();

        for (int i = 0; i < threadCount; i++) {
            Thread thread = new Thread(() -> {
                try {
                    // 模拟 IO 阻塞
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    latch.countDown();
                }
            });
            thread.start();
        }

        latch.await();
        long end = System.currentTimeMillis();
        System.out.println("Platform Threads: " + threadCount + " threads completed in " + (end - start) + "ms");
    }
}

注意:threadCount 设置为 10,000 时,平台线程可能勉强运行。如果设置 100,000,大部分机器会直接 OOM 或系统崩溃。

5.2 虚拟线程示例

import java.util.concurrent.CountDownLatch;

public class VirtualThreadTest {
    public static void main(String[] args) throws InterruptedException {
        int threadCount = 100_000;  // 10 万个虚拟线程,轻松应对
        CountDownLatch latch = new CountDownLatch(threadCount);
        long start = System.currentTimeMillis();

        for (int i = 0; i < threadCount; i++) {
            Thread vt = Thread.startVirtualThread(() -> {
                try {
                    // 模拟 IO 阻塞
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    latch.countDown();
                }
            });
        }

        latch.await();
        long end = System.currentTimeMillis();
        System.out.println("Virtual Threads: " + threadCount + " threads completed in " + (end - start) + "ms");
    }
}

结果对比:

| 指标 | 平台线程 (10,000) | 虚拟线程 (100,000) | |------|:----------------:|:-----------------:| | 启动时间 | 数秒 | 毫秒级 | | 内存占用 | ~1GB+ | ~几十 MB | | 是否 OOM | 可能 | 不会 | | 代码复杂度 | 需线程池管理 | 简单直接 |

六、虚拟线程的工作原理(简析)

虚拟线程的底层机制可以概括为:挂载(Mount)卸载(Unmount)

  1. 当一个虚拟线程开始执行时,JVM 会将其分配给一个 Carrier 线程(平台线程)。
  2. 当虚拟线程遇到阻塞操作(如 Thread.sleep()Socket.read()Future.get())时,JVM 会自动将其从 Carrier 线程上卸载,并保存其执行上下文。
  3. Carrier 线程被释放,可以去执行其他虚拟线程。
  4. 当阻塞操作完成时,虚拟线程会重新挂载到某个可用的 Carrier 线程上继续执行。

整个过程对开发者完全透明,我们写的是同步代码,但底层实现了类似异步 IO 的高效调度。

七、使用虚拟线程的注意事项

7.1 避免 synchronized 阻塞

synchronized 是 JVM 级别的锁,虚拟线程在进入 synchronized 块时不会被卸载,会导致 Carrier 线程被阻塞。建议使用 ReentrantLock 替代:

// 不推荐
synchronized (lock) {
    // critical section
}

// 推荐
private final Lock lock = new ReentrantLock();

lock.lock();
try {
    // critical section
} finally {
    lock.unlock();
}

7.2 不要池化虚拟线程

虚拟线程是轻量级的,创建和销毁的开销极低,不要将虚拟线程放入线程池中复用。直接用 Executors.newVirtualThreadPerTaskExecutor()Thread.startVirtualThread() 即可。

7.3 ThreadLocal 的合理使用

虚拟线程支持 ThreadLocal,但由于虚拟线程数量可能非常庞大,大量使用 ThreadLocal 会占用大量内存。如果确实需要上下文传递,建议考虑使用 ScopedValue(Java 21 中为孵化特性)。

八、总结

| 特性 | 说明 | |------|------| | ✅ 百万级并发 | 轻松创建数十万甚至百万级虚拟线程 | | ✅ 简化编程模型 | 使用同步代码即可实现高并发 | | ✅ 兼容性极佳 | 现有代码几乎无需修改 | | ✅ 零成本创建 | 创建和销毁开销极低 | | ⚠️ 慎用 synchronized | 会导致 Carrier 线程阻塞 | | ⚠️ 慎用线程池 | 虚拟线程不需要池化 | | ⚠️ 慎用 ThreadLocal | 海量虚拟线程下内存占用大 |

虚拟线程是 Java 并发编程的一次革命性升级。它让我们可以用 同步、顺序的代码 写出 高并发、高吞吐 的应用,彻底告别异步回调地狱。如果你的应用是 IO 密集型(Web 服务、微服务、数据库访问等),那么迁移到虚拟线程将是提升性能和简化代码的最佳选择。

从 Java 21 开始,虚拟线程已经是正式特性(非预览),建议所有 Java 开发者立即开始学习和尝试!

九、参考资料

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐