Java 21 已于2023年9月正式发布,其中虚拟线程(Virtual Threads)作为Project Loom的最终产出,彻底改变了JVM处理并发的方式。虚拟线程让开发者能够以近乎零成本的资源开销创建海量线程,从而编写简单、直观的“每任务一线程”代码,而无需担心传统平台线程的重量级限制。本文将通过一个构建高并发HTTP服务的完整案例,带你从零掌握虚拟线程的用法、结构化并发以及与Spring Boot的集成。
虚拟线程解决了什么问题?
在传统Java并发模型中,一个平台线程(OS线程)就是一个重量级资源,创建和切换成本高昂。当我们需要处理成千上万个并发请求时,不得不采用线程池和异步编程(如CompletableFuture、响应式框架)。这些模型虽然有效,但带来了代码复杂度陡增、调试困难和栈追踪混乱等问题。
虚拟线程是JVM内部管理的轻量级线程,它们在底层被多路复用到一个或多个平台线程上。当虚拟线程执行阻塞操作(如I/O、数据库调用)时,JVM会自动将其从平台线程上“卸下”,让该平台线程去执行其他虚拟线程,从而避免阻塞宝贵的OS线程。这使得我们可以安全地创建数百万个虚拟线程,每个连接一个线程,代码风格回到了简单同步编程,同时兼具异步的性能优势。
创建与管理虚拟线程的三种方式
Java 21 提供了多种创建虚拟线程的方式,这里展示最常用的三种。
方式一:通过 Thread.ofVirtual() 工厂方法
Thread vThread = Thread.ofVirtual()
.name("my-virtual-thread")
.start(() -> {
System.out.println("虚拟线程执行中: " + Thread.currentThread());
});
vThread.join(); // 等待虚拟线程完成
方式二:使用 Executors.newVirtualThreadPerTaskExecutor()
该方法返回一个为每个任务创建新虚拟线程的ExecutorService,非常适合“每任务一线程”的模式。
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> {
// 模拟I/O操作
Thread.sleep(Duration.ofMillis(100));
return "结果";
});
}
方式三:通过 Thread.Builder 构造更复杂的虚拟线程
Thread.Builder builder = Thread.ofVirtual()
.name("custom-", 0) // 前缀和起始编号
.unstarted(() -> System.out.println("未启动的虚拟线程"));
Thread t = builder.start();
t.join();
在实际开发中,方式二最常用,因为它完美替代了传统的线程池,且代码无需大改。
结构化并发:StructuredTaskScope实战
虚拟线程的另一大亮点是结构化并发(预览特性,需开启 –enable-preview)。它允许我们把一组相关任务组织在同一个作用域内,像管理方法调用一样管理线程生命周期,确保所有子任务完成或失败后统一处理。
以调用两个外部API并合并结果为例:
import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.Future;
String fetchUser() throws InterruptedException {
Thread.sleep(Duration.ofMillis(200));
return "用户数据";
}
String fetchOrder() throws InterruptedException {
Thread.sleep(Duration.ofMillis(150));
return "订单数据";
}
void process() throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> userFuture = scope.fork(this::fetchUser);
Future<String> orderFuture = scope.fork(this::fetchOrder);
scope.join(); // 等待所有任务完成
scope.throwIfFailed(); // 如果任何任务失败,抛出异常
String user = userFuture.resultNow();
String order = orderFuture.resultNow();
System.out.println("合并结果: " + user + " + " + order);
}
}
相比传统的CountDownLatch或CompletableFuture组合,结构化并发使代码的意图和错误处理更加清晰。注意该特性在Java 21中仍为预览,正式版本中API可能微调。
实战案例:构建高并发虚拟线程Web服务
我们将用虚拟线程实现一个简单的HTTP服务器,模拟查询数据库(睡眠)后返回响应。这个案例完整展示了如何用虚拟线程处理每个HTTP连接,而无需任何线程池配置。
首先,创建一个基于虚拟线程的HTTP服务:
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.*;
public class VirtualThreadWebServer {
private static final int PORT = 8080;
public static void main(String[] args) throws IOException {
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
System.out.println("虚拟线程Web服务器启动,监听端口 " + PORT);
// 关键:使用虚拟线程执行器处理每个连接
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
while (true) {
Socket clientSocket = serverSocket.accept();
executor.submit(() -> handleRequest(clientSocket));
}
}
}
}
private static void handleRequest(Socket socket) {
try (socket;
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
PrintWriter writer = new PrintWriter(socket.getOutputStream(), true)) {
// 读取HTTP请求行
String requestLine = reader.readLine();
System.out.println("收到请求: " + requestLine +
" [线程: " + Thread.currentThread() + "]");
// 模拟耗时操作(数据库查询、远程调用)
Thread.sleep(Duration.ofMillis(50));
// 构建HTTP响应
String responseBody = "{"message": "Hello from Virtual Thread!"}";
writer.println("HTTP/1.1 200 OK");
writer.println("Content-Type: application/json");
writer.println("Content-Length: " + responseBody.getBytes().length);
writer.println();
writer.println(responseBody);
} catch (Exception e) {
System.err.println("处理请求异常: " + e.getMessage());
}
}
}
现在,你可以使用Apache Bench或wrk进行压力测试。即使同时涌入数千个并发连接,服务器也不会因为线程数限制而崩溃。每个连接对应一个虚拟线程,I/O阻塞时JVM自动将底层平台线程释放给其他虚拟线程使用。
如果你想在此基础上添加线程本地变量,注意虚拟线程完全支持ThreadLocal,但由于虚拟线程数量庞大,应谨慎避免使用ThreadLocal存储大对象以防止内存溢出——Java 21中ThreadLocal仍然工作,但推荐使用ScopedValue(仍在预览)作为更轻量级的替代方案。
Spring Boot 3.x 集成虚拟线程配置
Spring Boot 3.2 开始对虚拟线程提供了开箱即用的支持。只需在配置文件中添加一个属性,即可让Tomcat和Jetty使用虚拟线程处理请求。
在 application.properties 或 application.yml 中启用:
# application.yml
spring:
threads:
virtual:
enabled: true
或者使用Properties格式:
# application.properties
spring.threads.virtual.enabled=true
一旦启用,Spring Boot 会自动将内嵌服务器(Tomcat/Jetty/Undertow)的工作线程池替换为虚拟线程执行器。同时,@Async 异步方法也会默认使用虚拟线程,无需额外配置。
下面是一个完整的Spring Boot控制器示例,展示如何利用虚拟线程处理高并发请求:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderController {
@GetMapping("/api/orders")
public String getOrders() throws InterruptedException {
// 模拟耗时数据库查询
Thread.sleep(Duration.ofMillis(80));
return "订单列表";
}
@GetMapping("/api/users")
public String getUsers() throws InterruptedException {
Thread.sleep(Duration.ofMillis(120));
return "用户列表";
}
}
在启用虚拟线程的情况下,上述每个请求都会在一个全新的虚拟线程中执行,线程切换几乎无开销。即使成百上千个请求同时到达,也不会因为平台线程耗尽而导致拒绝服务。
虚拟线程 vs 传统线程池性能对比
为了直观理解差异,我们编写一个简单的对比测试:分别使用固定线程池(200个平台线程)和虚拟线程执行器处理10000个并发任务,每个任务模拟100毫秒的网络I/O。测试代码如下:
import java.time.Duration;
import java.util.concurrent.*;
public class CompareTest {
public static void main(String[] args) throws Exception {
int taskCount = 10_000;
Runnable task = () -> {
try {
Thread.sleep(Duration.ofMillis(100)); // 模拟I/O
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
};
// 传统线程池(200个平台线程)
ExecutorService platformPool = Executors.newFixedThreadPool(200);
long start = System.currentTimeMillis();
for (int i = 0; i < taskCount; i++) {
platformPool.submit(task);
}
platformPool.shutdown();
platformPool.awaitTermination(10, TimeUnit.MINUTES);
long platformTime = System.currentTimeMillis() - start;
System.out.println("平台线程池耗时: " + platformTime + " ms");
// 虚拟线程执行器
try (ExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor()) {
start = System.currentTimeMillis();
for (int i = 0; i < taskCount; i++) {
virtualExecutor.submit(task);
}
// 虚拟线程执行器会在所有任务完成后关闭
}
long virtualTime = System.currentTimeMillis() - start;
System.out.println("虚拟线程耗时: " + virtualTime + " ms");
}
}
在典型的多核机器上,虚拟线程的执行时间通常远低于平台线程池,因为虚拟线程可以动态利用所有可用的平台线程,而200个平台线程在10000个任务时会出现严重的任务排队和上下文切换开销。更重要的是,整个代码结构保持了同步阻塞风格,完全没有回调地狱。
最佳实践与迁移策略
虚拟线程已经足够稳定用于生产环境,但在迁移现有应用时,需要注意以下几点:
- 不要池化虚拟线程:虚拟线程极其廉价,每次创建新线程即可,避免引入不必要的池化逻辑。
- 谨慎使用synchronized和本地方法:当虚拟线程执行同步块(synchronized)或JNI本地代码时,可能会钉住(pin)底层的平台线程,导致该线程被阻塞,影响吞吐量。应尽量用
ReentrantLock替代synchronized。 - 用结构化并发管理任务关系:一旦结构化并发正式发布,优先使用它来替代手动线程管理,以获得更好的取消和错误处理。
- 监控虚拟线程数量:虽然可以创建海量虚拟线程,但仍需关注内存占用。必要时使用信号量进行限流。
- 配合Spring Boot 3.2+快速入手:对于Web应用,直接启用
spring.threads.virtual.enabled即可获得立竿见影的性能提升。
虚拟线程并非要完全取代响应式编程。对于CPU密集型任务或需要精细控制背压的场景,响应式(如WebFlux)仍具备优势。但对于绝大多数I/O密集型的业务系统,虚拟线程提供了更简单的编程模型和足够的性能。
总结
Java 21的虚拟线程是并发编程史上的里程碑。它让“每任务一线程”的简单模型重新回归主流,同时解决了大规模并发的性能瓶颈。通过本文的实战案例,你已掌握虚拟线程的创建方式、结构化并发的基本用法、自定义Web服务器的构建方法以及Spring Boot的集成配置。立即在你的下一个Java项目中将线程池切换为虚拟线程执行器,亲身体验简洁代码与高性能的完美结合。

