1. 什么是虚拟线程

虚拟线程(Virtual Threads) 是 JDK 21(JEP 444)正式提供的轻量级线程,由 JVM 调度,挂载在少量 载体线程(carrier / platform thread) 上。

维度 平台线程 虚拟线程

映射

1:1 OS 线程

多对少,JVM 调度

创建成本

高(~MB 栈、内核参与)

低,可百万级

适用场景

CPU 密集、需固定线程亲和

I/O 密集、高并发阻塞代码

编程模型

传统 Thread

同步阻塞写法 + 高并发

适合:HTTP/RPC、JDBC、文件/网络阻塞 I/O。
不适合:长时间 CPU 计算(应交给 ForkJoinPool / 专用池)、大量 synchronized + 阻塞 I/O(pinning)、依赖 ThreadLocal 大量泄漏的场景。

2. 核心 API 与代码示例

2.1 创建

// Java 21+

Thread vThread = Thread.ofVirtual().name("worker-", 0).start(() -> {
    System.out.println("on virtual thread: " + Thread.currentThread());
});

// 未 start,手动 start

Thread vt = Thread.ofVirtual().name("api-call").unstarted(() -> callRemote());

vt.start();

vt.join();

// 工厂:批量命名

Thread.Builder builder = Thread.ofVirtual().name("pool-", 0);

2.2 ExecutorService

try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {

    IntStream.range(0, 10_000).forEach(i ->
        executor.submit(() -> {
            String body = httpClient.send(request, HttpResponse.BodyHandlers.ofString()).body();
            return body.length();

       })
    );
} // try-with-resources 会 shutdown 并等待

每个任务一个虚拟线程,无需像传统线程池那样调 corePoolSize/maxPoolSize 来扛并发。

2.3 Thread.startVirtualThread

Thread.startVirtualThread(() -> {
    try (var in = Files.newInputStream(path)) {
      // 阻塞 I/O 在虚拟线程上通常没问题
    } catch (IOException e) {
        throw new UncheckedIOException(e);
    }
});

2.4 从平台线程切到虚拟线程

ExecutorService virtualExec = Executors.newVirtualThreadPerTaskExecutor();
    virtualExec.submit(() -> {
    // 此处 Thread.currentThread().isVirtual() == true
    jdbcTemplate.query("SELECT ...", rs -> { /* 阻塞 */ });
});

2.5 StructuredTaskScope

并行子任务、失败时取消兄弟任务:

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Subtask<String> user = scope.fork(() -> fetchUser(id));
    Subtask<String> order = scope.fork(() -> fetchOrder(id));
    scope.join(); // 等待全部
    scope.throwIfFailed(); // 任一失败则传播
    return user.get() + order.get();
}

适合「一个请求里多个下游调用」;注意 API 在 JDK 22/23 有包名与类型调整,以你使用的 JDK 文档为准。

2.6 与 CompletableFuture 配合

ExecutorService vtExec = Executors.newVirtualThreadPerTaskExecutor();
CompletableFuture<String> a = CompletableFuture.supplyAsync(() -> callA(), vtExec);
CompletableFuture<String> b = CompletableFuture.supplyAsync(() -> callB(), vtExec);
String result = a.thenCombine(b, (x, y) -> x + y).join();

要点:异步链路的 执行器 要显式指定虚拟线程执行器,否则默认可能仍用 ForkJoinPool.commonPool()(平台线程)。

2.7 检测与调试

Thread t = Thread.currentThread();
t.isVirtual(); // true / false
// 启动参数:发现 pinning
// -Djdk.tracePinnedThreads=short

3. 使用时注意事项

3.1 Pinning(载体线程被占住)

在虚拟线程上进入 synchronized 或 native 方法里的阻塞 时,载体线程无法去跑别的虚拟线程,并发能力会掉下去。

// 风险:synchronized 块内有阻塞 I/O
synchronized (lock) {
    socket.getInputStream().read(); // 可能 pin 住 carrier
}

// 更稳妥:ReentrantLock + 可中断/限时,或换无锁/分段锁
private final ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
   // 仍要避免长时间阻塞;优先用虚拟线程友好的库
} finally {
   lock.unlock();
}

排查:-Djdk.tracePinnedThreads=short,并升级 HikariCP 5+、HttpClient 5、较新 JDBC/驱动 等已减少 pinning 的组件。

3.2 线程池 ≠ 连接池

虚拟线程可以很多,但 数据库/Redis/下游 HTTP 连接池 仍受下游限制。应按下游容量设 maximumPoolSize,而不是按虚拟线程数量设。

3.3 ThreadLocal

每个虚拟线程仍可有 ThreadLocal;百万虚拟线程 + 大对象 ThreadLocal = 内存风险。框架里大量隐式 ThreadLocal(如部分老 MVC/日志上下文)要审计。

3.4 不要池化虚拟线程

官方建议:每个任务 newVirtualThreadPerTaskExecutor(),不要搞「虚拟线程池大小 = 200」这种传统池思维(除非特殊场景用 Semaphore 限流)。

3.5 Thread.sleep / 阻塞 I/O

虚拟线程在 sleep、NIO、BlockingQueue.take 等会 unmount,载体可复用——这是相对平台线程的核心优势。

4. Spring Boot 启用虚拟线程前后对比

4.1 开关前后,运行时到底变了什么

项目 未开启时 开启后

请求线程

Tomcat/Jetty 平台线程池

每个请求一个 虚拟线程

@Async / Task 执行

ThreadPoolTaskExecutor(有界池)

SimpleAsyncTaskExecutor

业务代码

同步阻塞

不变,仍是同步阻塞

单次请求 CPU 时间

不变

不变

瓶颈

线程池满 → 排队

 DB/HTTP 连接池、下游、GC

前提:Java 21+、Spring Boot 3.2+(Spring Framework 6.1+)。

spring.threads.virtual.enabled=true

4.2 吞吐量:按并发度分三档(典型 I/O 型 REST)

场景多为:Controller → JDBC/HTTP 下游,单次请求里有 几十~几百 ms 阻塞等待(社区压测常见设定,非官方基准)。

低并发(约 100 并发用户)

指标 平台线程池 虚拟线程 变化

吞吐 (req/s)

~950

~980

约 +0~5%

P95 延迟

~105 ms

~102 ms

几乎一样

原因:线程池还没打满,平台线程够用,虚拟线程优势体现不出来。

中高并发(约 500~1000,池大小 ~200)

指标 平台线程 (pool≈200) 虚拟线程 变化

吞吐 (req/s)

~1,240~1,890

~2,650~3,850

约 +40%~2.3×

平均延迟

~264~805 ms

~55~190 ms

明显下降

P99

~430~1,450 ms

~92~310 ms

明显下降

原因:平台线程池 排队;虚拟线程在阻塞 I/O 时 unmount,载体可接更多请求,吞吐不再被「200 个 OS 线程」卡死。

另一组相近的 MVC 对比(4 vCPU、I/O 为主):

配置 吞吐 (req/s) P99

平台线程池 200

~1,850

~420 ms

虚拟线程

~4,200

~180 ms

WebFlux(参考)

~4,350

~175 ms

结论:I/O 密集的 Spring MVC,虚拟线程吞吐可接近 WebFlux,代码仍是阻塞风格。

极高并发(5000+ 并发 / 万级连接)

指标 平台线程 虚拟线程

吞吐

随排队 崩溃或持平很低

仍能维持 数千~近万 req/s(视下游而定)

极端情况

线程过多 → OOM / 拒绝

仍可能跑,但受 连接池、DB、网络 限制

有压测报告:1000 并发时平台线程 ~980 req/s、虚拟线程 ~3850 req/s(约 4×);10000 并发时平台线程失败、虚拟线程仍 ~9650 req/s——这类数字 强依赖 机器、池配置、是否 pinning,只能作量级参考。

4.3 什么时候吞吐几乎不变甚至变差

CPU 密集(计算占主导)

配置 吞吐 (req/s) 示例

平台线程池 ≈ CPU 核数 (8)

~12,000

虚拟线程

~9,800(更低)

虚拟线程 不会让CPU 算得更快;并发超过核数还会增加调度开销。

混合负载(短 CPU + I/O)

500 并发示例:平台线程 ~1890 req/s → 虚拟线程 ~2680 req/s(约 +40%),不如纯 I/O 夸张,但通常仍有收益。

5. 与 goroutine的性能与模型对比

维度 Java 虚拟线程 Go goroutine

调度者

JVM(ForkJoin 载体池)

Go runtime(G-M-P)

创建方式

Thread.ofVirtual() / Executor

go func()

语法风格

仍是 Java 线程 API

语言级 go关键字

阻塞 I/O

unmount(未 pinning 时)

网络 I/O 配合 netpoller,阻塞成本低

synchronized 有 pinning 风险

无 JVM pinning;但锁竞争仍影响吞吐

跨「线程」传数据

共享堆、需注意可见性

推荐 channel;也可共享内存

单请求延迟

不会因虚拟线程变快

同样,协程不加速单次 CPU 计算

高并发 I/O 吞吐

相对平台线程显著提升

长期以高并发 I/O 著称

性能结论(经验性,非绝对):

  1. I/O 密集、同步代码:Java 21 + 虚拟线程 与 Go 在「可支撑的并发连接数 / 吞吐」上往往 同一量级,差距更多来自框架、GC、序列化、连接池配置,而非「协程 vs 虚拟线程」本身。
  2. CPU 密集:Go 的 goroutine 与 Java 虚拟线程 都不会魔法加速;Go 常略占优在调度轻量、无 pinning;Java 应用应把 CPU 工作放在 平台线程池 / ForkJoinPool
  3. 内存:两者都可支撑大量并发任务;Java 需注意 堆对象 + ThreadLocal + 线程栈(虚拟线程栈在堆上但仍占空间);Go 需注意 goroutine 栈增长与 channel 缓冲。
  4. 生态习惯:Go 默认 go + channel;Java 传统是阻塞 + 线程,虚拟线程让 Spring 式同步代码 无需全面 Reactive 即可获得类似扩展性。

6. 各流行 Java 框架对虚拟线程的支持

框架 / 组件 支持情况 启用方式 / 说明

JDK 21+

正式(JEP 444)

无需预览开关

Spring Boot 3.2+

一等公民,全局开关

spring.threads.virtual.enabled=true(需 Java 21+);Tomcat/Jetty 请求、@Async、Task 执行/调度默认走虚拟线程

Spring Framework 6.1+

与 Boot 配套

TaskExecutor / SimpleAsyncTaskExecutor 虚拟线程变体

Spring WebFlux

已是 Reactor 非阻塞

虚拟线程主要惠及 WebMVC(阻塞);WebFlux 不必为了并发强行切

Quarkus 3.x

显式、按方法

@RunOnVirtualThread;扩展 quarkus-virtual-threads;底层仍是 Vert.x/Netty,虚拟线程是「跑阻塞代码」的叠加层

Micronaut 4.x

注解 + 实验特性

@ExecuteOn(TaskExecutors.VIRTUAL);4.9+ Loom carrier(实验)把 Netty 事件循环与载体结合

Tomcat 10.1+

支持 Executor 用虚拟线程

Spring Boot 开启后自动配置

Netty

4.2+ 对 pinning 有改进

常作底层;业务阻塞代码放虚拟线程

Hibernate / JDBC

阻塞 API 可用

注意连接池大小与 pinning;用较新驱动

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐