Java 虚拟线程(Virtual Threads)
Java 虚拟线程(Virtual Threads)实战
在 Java 长期发展历程中,线程模型几乎没有颠覆性变化——直到虚拟线程(Virtual Threads)的出现。作为 Java 19 引入的预览特性(JEP 425)并在 Java 21 正式转正,虚拟线程彻底改变了我们处理并发任务的方式。本文将从实战角度出发,带你理解虚拟线程的原理、使用场景,并通过代码示例展示其如何轻松支撑百万级并发。
一、虚拟线程是什么?—— 打破传统线程的桎梏
传统线程的困境
Java 中的 Thread 长期以来都是平台线程(Platform Thread) 的封装,它与操作系统内核线程一一对应。这种模型有两个致命缺陷:
-
资源昂贵:每个平台线程会占用 1-2MB 栈内存,创建数千个线程就可能耗尽系统资源。
-
阻塞浪费:当线程因 IO 操作(如数据库查询、网络请求)阻塞时,对应的内核线程也会闲置,造成资源浪费。
这就是为什么我们需要线程池来限制线程数量,但线程池本质上是一种妥协——它通过限制并发量来避免资源耗尽,却也限制了系统的吞吐量。
虚拟线程的突破
虚拟线程是 Java 虚拟机(JVM)管理的用户态线程,它与内核线程不再是一一对应关系:
-
轻量级:单个虚拟线程的栈内存可以动态伸缩(从几 KB 开始),理论上可创建数百万个。
-
M:N 调度:多个虚拟线程映射到少量平台线程上运行,当虚拟线程阻塞时,JVM 会将其挂载(unpark),释放平台线程供其他虚拟线程使用。
简单说,虚拟线程让我们可以像创建对象一样轻松创建线程,彻底摆脱线程数量的束缚。
二、快速上手:3 种创建虚拟线程的方式
虚拟线程的 API 设计非常友好,与传统线程兼容,学习成本极低。以下是最常用的创建方式:
方式 1:通过 Thread.startVirtualThread() 创建
这是创建单个虚拟线程最直接的方式:
// 创建并启动虚拟线程
Thread virtualThread = Thread.startVirtualThread(() -> {
System.out.println("虚拟线程运行中...");
System.out.println("线程类型: " + Thread.currentThread().getClass().getSimpleName());
});
// 等待虚拟线程完成
virtualThread.join();
输出结果:
虚拟线程运行中...
线程类型: VirtualThread
方式 2:通过 Thread.Builder 定制虚拟线程
如需设置线程名、异常处理器等属性,可使用构建器模式:
Thread virtualThread = Thread.ofVirtual()
.name("order-process-1") // 设置线程名
.uncaughtExceptionHandler((t, e) -> {
System.err.println("线程 " + t.getName() + " 发生异常: " + e.getMessage());
})
.start(() -> {
System.out.println("处理订单...");
// 模拟业务逻辑(如数据库查询、网络请求等IO操作)
try {
Thread.sleep(Duration.ofSeconds(5));
System.out.println("任务执行完成");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("任务被中断");
}
});
virtualThread.join();
方式 3:通过 ExecutorService 批量管理虚拟线程
在实际开发中,我们通常需要管理多个虚拟线程,此时可使用
// 创建虚拟线程池(每个任务对应一个虚拟线程)
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
// 提交 1000 个任务
for (int i = 0; i < 1000; i++) {
int taskId = i;
executor.submit(() -> {
System.out.println("执行任务 " + taskId + ",线程: " + Thread.currentThread().getName());
// 模拟 IO 操作
Thread.sleep(Duration.ofMillis(100));
return taskId;
});
}
}catch (Exception e) {
e.printStackTrace();
}
这个例子中,我们轻松创建了 1000 个并发任务,而系统资源几乎没有压力——这在传统线程模型中是难以想象的。
三、性能对比:虚拟线程 vs 平台线程
为了直观感受虚拟线程的优势,我们设计一个简单的性能测试:模拟高并发 IO 场景(每个任务包含 100ms 的模拟网络请求),对比虚拟线程与平台线程的吞吐量。
测试代码
package com.sucl.virtualthread.test;
import org.junit.jupiter.api.Test;
import java.time.Duration;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ThreadPerformanceTest {
// 测试任务数量
private static final int TASK_COUNT = 10_000;
// 每个任务的模拟 IO 等待时间
private static final Duration IO_WAIT = Duration.ofMillis(100);
@Test
public void test() throws InterruptedException {
// 测试平台线程池(核心线程数 100)
testPlatformThreads();
// 测试虚拟线程池
testVirtualThreads();
}
private static void testPlatformThreads() throws InterruptedException {
long start = System.currentTimeMillis();
// 创建固定大小的平台线程池
try (ExecutorService executor = Executors.newFixedThreadPool(100)) {
for (int i = 0; i < TASK_COUNT; i++) {
executor.submit(() -> {
try {
// 模拟 IO 操作(如数据库查询、网络请求)
Thread.sleep(IO_WAIT);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
}
long end = System.currentTimeMillis();
System.out.printf("平台线程完成 %d 个任务,耗时: %d ms%n",
TASK_COUNT, end - start);
}
private static void testVirtualThreads() throws InterruptedException {
long start = System.currentTimeMillis();
// 创建虚拟线程池
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < TASK_COUNT; i++) {
executor.submit(() -> {
try {
// 模拟 IO 操作
Thread.sleep(IO_WAIT);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
}
long end = System.currentTimeMillis();
System.out.printf("虚拟线程完成 %d 个任务,耗时: %d ms%n",
TASK_COUNT, end - start);
}
}
测试结果(JDK 21,8 核 CPU)
平台线程完成 10000 个任务,耗时: 10912 ms
虚拟线程完成 10000 个任务,耗时: 456 ms
结果分析:
-
平台线程池因受限于 100 个核心线程,需要分 100 批执行(10000/100),总耗时约为 100 * 100ms = 10 秒。
-
虚拟线程几乎可以同时执行所有任务,总耗时仅略大于单个任务的 IO 等待时间(100ms),吞吐量提升近 8 倍!
四、实战场景:虚拟线程在 Web 服务中的应用
虚拟线程最适合 IO 密集型场景,如 Web 服务、微服务调用、数据库操作等。下面以 Spring Boot 应用为例,展示如何用虚拟线程提升接口吞吐量。
步骤 1:升级 Spring Boot 版本
Spring Boot 3.2+ 对虚拟线程提供了原生支持,在 pom.xml 中配置:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
步骤 2:启用虚拟线程
在 application.properties 中配置,让 Tomcat 使用虚拟线程处理请求:
# 启用虚拟线程
spring.threads.virtual.enabled=true
# Tomcat 连接器使用虚拟线程
server.tomcat.threads.virtual.enabled=true
步骤 3:编写控制器测试
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
@RestController
public class VirtualThreadController {
// 模拟数据库查询的 IO 操作
private String fetchDataFromDb() throws InterruptedException {
// 模拟 100ms 的数据库查询
Thread.sleep(Duration.ofMillis(100));
return "数据库查询结果";
}
// 使用虚拟线程处理请求(Spring 会自动分配)
@GetMapping("/api/data")
public CompletableFuture<String> getData() {
// 用 CompletableFuture 包装异步操作
return CompletableFuture.supplyAsync(() -> {
try {
return fetchDataFromDb();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return "查询中断";
}
});
}
}
性能测试(使用 JMeter 模拟 10000 并发请求)
|
线程模型 |
平均响应时间 |
吞吐量(QPS) |
错误率 |
|---|---|---|---|
|
平台线程 |
876ms |
1140 |
3.2% |
|
虚拟线程 |
123ms |
8130 |
0% |
结论:在 Web 服务场景中,虚拟线程能显著降低响应时间并提升吞吐量,尤其在高并发下优势明显。
五、避坑指南:虚拟线程的使用注意事项
虽然虚拟线程强大,但并非银弹,使用时需注意以下几点:
-
不适合 CPU 密集型任务
虚拟线程的优势在于处理 IO 阻塞,对于纯计算任务(如大数据量排序),其性能与平台线程无异,甚至因调度开销略差。 -
避免同步代码块滥用
虚拟线程数量极多,若大量虚拟线程争夺同一把锁,会导致频繁的上下文切换。建议使用ReentrantLock替代synchronized,或采用无锁设计。 -
注意线程局部变量(ThreadLocal)
虚拟线程会继承平台线程的ThreadLocal,若ThreadLocal中存储大对象,可能导致内存泄漏。建议使用InheritableThreadLocal或框架提供的上下文传递机制(如 Spring 的RequestContextHolder)。 -
不要设置过大的栈大小
虚拟线程默认栈大小动态伸缩,手动设置-Xss可能适得其反,浪费资源。 -
兼容问题排查
部分老旧库(如某些 JDBC 驱动、日志框架)可能依赖平台线程的特性,使用前需测试兼容性。可通过Thread.currentThread().isVirtual()区分线程类型,针对性处理。
六、总结:虚拟线程带来的变革
虚拟线程不是对传统线程的小修小补,而是 Java 并发模型的一次革命性升级。它让开发者可以用简单的编程模型实现高并发,无需再为线程池参数调优、线程数量限制而烦恼。
-
对于开发者:可以更专注于业务逻辑,用“一个任务一个线程”的直观方式编写代码。
-
对于系统:能更高效地利用硬件资源,尤其在云原生环境中,可显著降低资源成本。
虚拟线程的普及可能需要时间,但现在正是学习和实践的最佳时机。不妨从一个简单的 IO 密集型任务开始,体验百万级并发的丝滑感受!
更多推荐



所有评论(0)