CodeGym /课程 /JAVA 25 SELF /Virtual Threads 的可伸缩性与性能

Virtual Threads 的可伸缩性与性能

JAVA 25 SELF
第 57 级, 课程 2
可用

1. 可伸缩性

为什么普通线程的可伸缩性差?

每个传统线程(Thread)都是一个具有独立栈(通常为 1–2 MB)与状态结构的操作系统实体。尝试创建例如 10 000 个普通线程,往往会触发 OutOfMemoryError。因此传统服务器通常使用有限的线程池。

虚拟线程:可伸缩性的魔法

虚拟线程(Java 21+)是由 JVM 管理的“轻量级”线程。其栈存放在堆上,可按需伸缩。当线程在 I/O 上阻塞时,JVM 会“冻结”它并切换去执行其他任务。

JVM 内部运行着一个小型“载体线程”池(carrier threads)——这些是操作系统的平台线程,虚拟线程会在其上轮流执行。这使得创建 100 000+ 个任务也不会带来内存灾难。JVM 会自行规划何时执行哪些虚拟线程。

演示:100_000 个虚拟线程 vs 1_000 个平台线程

示例:创建 1000 个普通线程

// 试图创建 1000 个普通线程
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
    Thread t = new Thread(() -> {
        try {
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    });
    threads.add(t);
    t.start();
}
System.out.println("已创建线程数:" + threads.size());

结果:在大多数系统上可创建 1 000–2 000 个线程;更大的数量会引发内存问题与明显卡顿。

示例:创建 100 000 个虚拟线程

// 创建 100_000 个虚拟线程
List<Thread> vThreads = new ArrayList<>();
for (int i = 0; i < 100_000; i++) {
    Thread t = Thread.ofVirtual().start(() -> {
        try {
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    });
    vThreads.add(t);
}
System.out.println("已创建虚拟线程数:" + vThreads.size());

结果:程序可轻松创建 100 000 个虚拟线程而不会崩溃,也无明显卡顿。所需内存大幅降低。

可视化对比

线程类型 最大线程数(约) 内存占用 启动时间
普通(Thread) 1 000 – 10 000 高 较长
虚拟 100 000 – 1 000 000+ 低 几乎瞬时

事实:虚拟线程让“每个任务一个线程”的编码方式成为可能,无需复杂线程池,也不易压垮系统。

2. 性能:虚拟线程的优势适用场景

以 I/O 为瓶颈的任务(I/O-bound)

虚拟线程非常适合网络请求、文件 I/O 与数据库操作。当操作阻塞时,虚拟线程会释放“载体线程”,JVM 则去执行其他任务。对于大量并发等待的场景,这能显著提升吞吐。

示例:模拟 10 000 个并发 HTTP 请求

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

HttpClient client = HttpClient.newHttpClient();
List<Thread> threads = new ArrayList<>();

for (int i = 0; i < 10_000; i++) {
    Thread t = Thread.ofVirtual().start(() -> {
        try {
            HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://example.com"))
                .build();
            HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
            System.out.println("响应:" + response.statusCode());
        } catch (Exception e) {
            System.out.println("错误:" + e.getMessage());
        }
    });
    threads.add(t);
}

// 等待所有线程结束
for (Thread t : threads) {
    t.join();
}

结果:所有 10 000 个请求并行执行;程序不会崩溃,代码保持简洁。

CPU-bound:虚拟线程并不会加速计算

若任务主要消耗 CPU,虚拟线程不会带来速度提升:内核数量是固定的。此时应使用与核心数相等的固定线程池,以避免过度竞争。

// 每个任务都对一个很大的范围求和
Runnable cpuTask = () -> {
    long sum = 0;
    for (int i = 0; i < 100_000_000; i++) {
        sum += i;
    }
    System.out.println("求和结果:" + sum);
};

// 启动 1000 个执行计算的虚拟线程
for (int i = 0; i < 1000; i++) {
    Thread.ofVirtual().start(cpuTask);
}

结果:线程会争夺 CPU,但并不会更快——这不是 Virtual Threads 的用武之地.

3. 限制与注意事项

同步与虚拟线程的坑

  • 谨慎使用本机阻塞/锁。使用 synchronized 可能会将虚拟线程固定到载体线程上(pin),从而降低收益。优先使用 ReentrantLock、Semaphore 以及 java.util.concurrent 中为虚拟线程优化的其他原语。
  • 老旧库。某些 JDBC 驱动与本机库尚未针对 Virtual Threads 优化。务必仔细测试所有阻塞操作。

不适合长生命周期任务

Virtual Threads 非常适合“短工”:处理一个请求、执行一次操作然后结束。创建百万个永不结束的任务(例如无休止的计算)并无意义——此类场景请使用平台线程。

4. 最佳实践:何时使用虚拟线程

  • I/O-bound 任务:网络调用、文件、数据库——凡是线程经常处于等待的场景。
  • Web 服务器:为每个 HTTP 请求使用一个独立的虚拟线程。
  • 集成测试:快速模拟成千上万的客户端。
  • 异步处理:可以继续书写熟悉的“阻塞式”代码——JVM 会进行智能调度。

不建议使用:

  • 持续高 CPU 负载的任务。
  • 对低层库兼容性要求很高的场景(并非所有库都已适配)。

推荐在内部使用执行器:Executors.newVirtualThreadPerTaskExecutor()——“每个任务一个虚拟线程”,无需固定线程池。

5. 监控与度量:如何观测虚拟线程

JVisualVM 与 Flight Recorder

JVisualVM 可展示活动线程、其状态与内存;从 Java 21 起虚拟线程会单独显示。Java Flight Recorder(JFR)记录详尽的“黑盒”运行数据,包括 Virtual Threads 的统计信息——非常适合定位瓶颈。

在代码中查看线程数量

查看 JVM 中线程数量的简单方法:

System.out.println("线程总数:" + Thread.activeCount());

统计其中有多少是虚拟线程:

long vCount = Thread.getAllStackTraces().keySet().stream()
    .filter(Thread::isVirtual)
    .count();
System.out.println("虚拟线程数:" + vCount);

6. 使用虚拟线程的常见错误

错误 1:将虚拟线程用于重型计算。为 CPU-bound 任务启动数百万个虚拟线程不会让处理器更快。Virtual Threads 不是计算“加速器”。

错误 2:盲目沿用旧模式。不要为虚拟线程创建固定大小的线程池。请使用 Executors.newVirtualThreadPerTaskExecutor(),让 JVM 自动扩展。

错误 3:使用未适配的库。本机阻塞以及未针对 Loom 适配的库可能导致卡顿与性能下滑。请提前检查兼容性。

错误 4:过早优化。如果你只有少量线程、常规并发能满足需求,不必急于全面迁移到 Virtual Threads。此工具主要适用于大量 I/O 与等待的场景。

错误 5:忽视监控。创建上百万个任务很容易,但如果缺少监控与异常处理,得到的可能只是“漂亮的基准测试”,而非可靠系统。请使用 JVisualVM 与 JFR。

1
任务
JAVA 25 SELF, 第 57 级, 课程 2
已锁定
虚拟城市人口监控 🏙️
虚拟城市人口监控 🏙️
1
任务
JAVA 25 SELF, 第 57 级, 课程 2
已锁定
云计算服务器负载模拟 ☁️
云计算服务器负载模拟 ☁️
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION