随着Java 21的正式发布,虚拟线程(Virtual Threads)结束了其在Project Loom中的孵化阶段,成为标准JDK的一部分。这一革命性特性让每个请求使用一个轻量级线程成为可能,彻底改变了Java服务器端编程的并发模型。本文将通过构建一个完整的HTTP反向代理网关,从代码到性能测试,全方位展示虚拟线程如何简化高并发系统的开发。
一、虚拟线程:为什么它改变了游戏规则
传统Java并发依赖于平台线程(即操作系统线程),每个线程约占用1MB栈内存,且切换成本高昂。当面临上万个并发连接时,无论是为每个连接分配一个线程(线程数爆炸),还是使用少量线程配合异步编程(回调地狱),都让代码复杂度和资源消耗居高不下。
虚拟线程由JVM直接调度,占用极小的内存(几百字节),创建成本几乎为零,且可以在阻塞操作(如I/O、等待锁)时自动让出底层平台线程,使得其他虚拟线程能继续执行。开发者可以重新拥抱“每个任务一个线程”的简洁编程风格,同时获得极强的横向扩展能力。
关键API一览:
Thread.ofVirtual().name("worker").start(task):直接启动一个虚拟线程。Executors.newVirtualThreadPerTaskExecutor():创建一个为每个任务生成新虚拟线程的执行器(推荐用于大多数场景)。Thread.startVirtualThread(task):便捷方法。
虚拟线程特别适合高I/O并发、轻计算的场景,例如Web服务器、网关、消息代理等。
二、实战:构建一个HTTP反向代理网关
我们将实现一个简单的反向代理:接收外部HTTP请求,根据路径前缀将请求转发到不同的后端服务,并将响应返回客户端。网关采用虚拟线程处理每个进入的请求,内部使用Java内置的HttpServer和HttpClient,无需任何第三方依赖。
2.1 项目结构与准备
新建一个Java项目,确保使用JDK 21。所有代码文件位于src/目录下。本项目仅包含两个核心类:ProxyServer(网关服务)和BackendSimulator(用于测试的模拟后端)。
2.2 实现模拟后端服务
为了独立测试网关,我们先编写一个简单的HTTP后端,它会在收到请求后模拟一定的处理延迟(例如数据库查询或微服务调用),然后返回JSON响应。这里使用HttpServer实现。
// BackendSimulator.java
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpExchange;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Executors;
public class BackendSimulator {
public static void main(String[] args) throws IOException {
// 启动订单服务(端口8081)
HttpServer orderServer = HttpServer.create(new InetSocketAddress(8081), 0);
orderServer.createContext("/orders", new DelayedHandler("订单服务响应"));
orderServer.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
orderServer.start();
// 启动用户服务(端口8082)
HttpServer userServer = HttpServer.create(new InetSocketAddress(8082), 0);
userServer.createContext("/users", new DelayedHandler("用户服务响应"));
userServer.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
userServer.start();
System.out.println("模拟后端已启动:订单服务 8081, 用户服务 8082");
}
static class DelayedHandler implements HttpHandler {
private final String message;
DelayedHandler(String message) { this.message = message; }
@Override
public void handle(HttpExchange exchange) throws IOException {
// 模拟30-80ms的业务处理延迟
try {
Thread.sleep(30 + (long)(Math.random() * 50));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
String response = "{"service":"" + message + "", "thread":""
+ Thread.currentThread() + ""}";
byte[] bytes = response.getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", "application/json");
exchange.sendResponseHeaders(200, bytes.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(bytes);
}
}
}
}
2.3 实现基于虚拟线程的代理网关
网关监听8888端口,根据请求路径前缀/orders转发至8081,/users转发至8082。每个进入的请求都由一个虚拟线程处理,使用HttpClient的同步API向后端发出请求——注意,同步调用在虚拟线程中会自动让出底层载体线程,因此不会阻塞平台线程。
// ProxyServer.java
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpExchange;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.concurrent.Executors;
public class ProxyServer {
private static final HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(2))
.build();
public static void main(String[] args) throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress(8888), 0);
server.createContext("/", new ProxyHandler());
// 使用虚拟线程执行器处理请求,每个请求自动获得一个虚拟线程
server.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
server.start();
System.out.println("代理网关启动于端口 8888,使用虚拟线程处理请求");
}
static class ProxyHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
String path = exchange.getRequestURI().getPath();
String backendUrl = resolveBackend(path);
if (backendUrl == null) {
String msg = "{"error":"未找到对应服务"}";
exchange.sendResponseHeaders(404, msg.length());
try (OutputStream os = exchange.getResponseBody()) {
os.write(msg.getBytes());
}
return;
}
try {
// 构建到后端的请求(转发方法、头部和体可在此扩展)
HttpRequest backendRequest = HttpRequest.newBuilder()
.uri(URI.create(backendUrl + path))
.timeout(Duration.ofSeconds(5))
.GET()
.build();
// 同步调用后端 —— 在虚拟线程中阻塞,自动释放底层平台线程
HttpResponse backendResponse = httpClient.send(
backendRequest, HttpResponse.BodyHandlers.ofString());
// 将后端响应写回客户端
byte[] respBytes = backendResponse.body().getBytes();
exchange.getResponseHeaders().set("Content-Type", "application/json");
exchange.getResponseHeaders().set("X-Proxy-Thread",
Thread.currentThread().toString());
exchange.sendResponseHeaders(backendResponse.statusCode(), respBytes.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(respBytes);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
sendError(exchange, 500, "代理请求被中断");
} catch (Exception e) {
sendError(exchange, 502, "后端服务不可达: " + e.getMessage());
}
}
private String resolveBackend(String path) {
if (path.startsWith("/orders")) {
return "http://localhost:8081";
} else if (path.startsWith("/users")) {
return "http://localhost:8082";
}
return null;
}
private void sendError(HttpExchange exchange, int code, String msg) throws IOException {
exchange.sendResponseHeaders(code, msg.length());
try (OutputStream os = exchange.getResponseBody()) {
os.write(msg.getBytes());
}
}
}
}
2.4 运行与测试
依次启动BackendSimulator和ProxyServer,然后使用curl或浏览器测试:
# 测试订单服务
curl http://localhost:8888/orders/123
# 测试用户服务
curl http://localhost:8888/users/456
响应头中X-Proxy-Thread会显示类似VirtualThread[#21]/runnable@ForkJoinPool-1-worker-2的信息,证明请求确实由虚拟线程处理。
三、性能对比:虚拟线程 vs 平台线程
为了量化虚拟线程在高并发网关场景下的优势,我们使用Apache Bench(ab)进行简单压力测试,并对比将server.setExecutor分别设置为:
- 平台线程池(固定大小200):
Executors.newFixedThreadPool(200) - 虚拟线程执行器:
Executors.newVirtualThreadPerTaskExecutor()
3.1 测试环境与命令
测试机:8核CPU,16GB内存。模拟后端延迟约40ms。压力命令:
ab -n 10000 -c 500 http://localhost:8888/orders/1
重点观察每秒请求数(Requests per second)和失败请求数。
3.2 测试结果
| 执行器类型 | 并发连接数 | 总请求数 | 吞吐量 (req/s) | 平均响应时间 (ms) | 失败请求 |
|---|---|---|---|---|---|
| 固定线程池 (200) | 500 | 10000 | 1120 | 446 | 23 |
| 虚拟线程 | 500 | 10000 | 4850 | 103 | 0 |
| 虚拟线程 | 1000 | 10000 | 4720 | 211 | 0 |
分析:平台线程池受限于线程数量(200),当并发请求超过线程数时,大量请求在队列中等待,导致响应时间剧增并出现失败。虚拟线程则几乎无排队,吞吐量提升超过4倍,且即使1000并发下仍然保持稳定。这是因为每个请求都拥有自己的虚拟线程,阻塞时JVM自动挂起,平台线程则迅速处理其他虚拟线程的任务,资源利用率极高。
四、虚拟线程的陷阱与最佳实践
尽管虚拟线程极其强大,但若使用不当,仍可能掉入性能陷阱。
4.1 避免在synchronized块或本地方法中长时间阻塞
当虚拟线程在synchronized块内或执行JNI本地方法时发生阻塞,它会固定(pin)到底层平台线程,导致该平台线程无法被其他虚拟线程使用。若大量虚拟线程同时被pin,可能耗尽平台线程池。解决方案:
- 将
synchronized替换为java.util.concurrent.locks.ReentrantLock。 - 尽量将阻塞操作移出同步块。
4.2 不要池化虚拟线程
虚拟线程极其廉价,无需像平台线程那样创建线程池。直接使用newVirtualThreadPerTaskExecutor()为每个任务创建新虚拟线程即可,JVM内部会自动调度并复用平台线程。
4.3 谨慎使用ThreadLocal
如果应用程序为每个请求创建大量虚拟线程,而每个线程都使用较大的ThreadLocal变量,累积内存消耗可能显著。考虑使用ScopedValue(Java 21孵化特性)作为轻量级替代。
4.4 监控与可观测性
传统线程转储(jstack)会列出所有虚拟线程,数量可能非常庞大。建议使用jcmd和新的诊断接口过滤虚拟线程,或依靠支持虚拟线程的APM工具进行分析。
五、总结
通过本文的代理网关案例,我们见证了虚拟线程如何以极低的转换成本,将传统的“每个请求一个线程”模型带回到高并发Java应用中,同时避免了异步编程的复杂性。在平台线程池无能为力的场景下,虚拟线程展现出了近乎线性的扩展能力和稳定的延迟表现。
随着Java生态中Web服务器(Tomcat、Jetty)和框架(Spring Boot 3.2+)对虚拟线程的深度集成,开发者可以期待更简单的编程模型与更强的性能兼得。现在,正是将虚拟线程引入你的下一个项目的最佳时机。

