引言
Java 19 引入虚拟线程(Virtual Threads)作为 Project Loom 的重要成果,这项技术被誉为 Java 并发编程的革命性进步。
能用更低的内存支持数百万并发任务,听起来特别美好,让开发者可以摆脱传统线程池的束缚。
结果,因为我们团队的盲目自信,觉得新技术好就直接上了,没成想,它结结实实地给我们上了一课——在生产环境中遭遇了一次由虚拟线程引发的严重故障。
这次经历让我深刻认识到一个道理:新技术再好,盲目使用也会带来意想不到的风险。
作者注:
本文基于真实的生产环境经验总结,部分业务、代码和配置做了脱敏处理
看似完美的性能优化
背景介绍
我们的核心业务系统是一个高并发的订单处理服务,日均处理订单量非常庞大。
原来是用的传统的线程池模型,在高峰期经常出现线程池耗尽的问题。监控数据能看出来,在处理复杂订单时,平均每个请求需要占用线程 200-300ms,而大部分时间都消耗在 I/O 等待上。
虚拟线程改造过程
自动升级到JDK21之后,我们团队就决定进行业务上的技术迭代,用虚拟线程池代替传统线程池。
改造过程非常简单:
// 改造前:传统线程池
@Configuration
public class ThreadPoolConfig {
@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(200);
executor.setMaxPoolSize(500);
executor.setQueueCapacity(1000);
return executor;
}
}
// 改造后:虚拟线程
@Configuration
public class VirtualThreadConfig {
@Bean
public TaskExecutor taskExecutor() {
return new TaskExecutor() {
private final ExecutorService virtualExecutor =
Executors.newVirtualThreadPerTaskExecutor();
@Override
public void execute(Runnable task) {
virtualExecutor.submit(task);
}
};
}
}
初期表现相当美好
改造完成后,系统表现确实令人惊喜:
- 内存使用率:从平均 70% 降至 45%
- 响应时间:P99 从 800ms 降至 400ms
- 并发处理能力:从 2000 QPS 提升至 8000 QPS
- 线程数量:从峰值 1000+ 降至稳定的几十个载体线程
这么完美的结果,让整个团队都相当自豪。
灾难降临
事故表现
距离上线几周后,公司运营部门开展大促活动。
流量开始激增时,一切看起来都很正常。然而,当并发量达到平时的 3 倍时,系统开始出现异常:
18:23 - 监控告警:部分订单处理超时
18:25 - 数据库连接池告警:连接数异常增长
18:27 - 系统整体响应时间飙升至 5s+
18:30 - 服务开始返回 500 错误
18:35 - 系统完全不可用
问题定位
1. 初步排查
最初怀疑是数据库性能问题,但通过阿里云监控面板来看,数据库本身性能正常。
问题出在连接数暴涨。
# 数据库连接数监控
mysql> show status like 'Threads_connected';
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| Threads_connected | 2048 | # 接近最大连接数限制
+-------------------+-------+
2. 分析业务日志
业务日志显示大量数据库连接超时错误:
2025-05-15 16:26:33.245 ERROR [virtual-thread-1234] c.e.OrderService -
获取数据库连接超时: HikariPool-1 - Connection is not available,
request timed out after 30000ms.
3. 关键发现
通过深入分析 JVM 线程栈和监控数据,我们发现了问题的根本原因:
// 问题代码片段
@Service
public class OrderService {
@Async // 使用虚拟线程执行
public CompletableFuture<OrderResult> processOrder(OrderRequest request) {
// 数据库查询
Order order = orderRepository.findByOrderNo(request.getOrderNo());
// 调用外部支付接口
PaymentResult paymentResult = paymentClient.processPayment(request);
// 更新订单状态
order.setStatus(paymentResult.getStatus());
orderRepository.save(order);
return CompletableFuture.completedFuture(new OrderResult(order));
}
}
原因分析
1. 连接池设计理念的冲突
传统线程池与数据库连接池的设计有一个前提:并发线程数量是有限且可控的。
// 传统配置
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setMaximumPoolSize(100); // 连接池大小
config.setConnectionTimeout(30000);
return new HikariDataSource(config);
}
}
// 对应的线程池配置
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setMaxPoolSize(200); // 线程数 vs 连接数比例约为 2:1
2. 虚拟线程的负面影响
虚拟线程的优势在于可以创建数百万个线程而不耗尽内存,但这也意味着:
// 虚拟线程场景下的问题
public void demonstrateProblem() {
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
// 瞬间创建 10000 个虚拟线程
for (int i = 0; i < 10000; i++) {
executor.submit(() -> {
// 每个虚拟线程都试图获取数据库连接
try (Connection conn = dataSource.getConnection()) {
// 执行数据库操作
performDatabaseOperation(conn);
}
});
}
}
在高并发场景下,成千上万的虚拟线程同时竞争有限的数据库连接,导致连接池迅速耗尽。
3. 监控盲区
传统的 JVM 监控工具对虚拟线程的可观测性支持还不够完善:
// 传统监控代码无法准确反映虚拟线程数量
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
int threadCount = threadBean.getThreadCount(); // 只显示载体线程数量
System.out.println("活跃线程数: " + threadCount); // 误导性信息
解决方案与最佳实践
1. 资源池重新设计
针对虚拟线程场景,需要重新评估各种资源池的配置:
@Configuration
public class VirtualThreadAwareConfig {
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
// 大幅增加连接池大小
config.setMaximumPoolSize(500);
config.setConnectionTimeout(10000); // 降低超时时间
config.setLeakDetectionThreshold(60000); // 开启连接泄露检测
return new HikariDataSource(config);
}
@Bean
public RestTemplate restTemplate() {
HttpComponentsClientHttpRequestFactory factory =
new HttpComponentsClientHttpRequestFactory();
// HTTP 连接池配置
RequestConfig requestConfig = RequestConfig.custom()
.setConnectionRequestTimeout(5000)
.setSocketTimeout(10000)
.build();
CloseableHttpClient httpClient = HttpClients.custom()
.setMaxConnTotal(1000) // 总连接数
.setMaxConnPerRoute(100) // 每个路由的连接数
.setDefaultRequestConfig(requestConfig)
.build();
factory.setHttpClient(httpClient);
return new RestTemplate(factory);
}
}
2. 流量控制机制
引入信号量(Semaphore)来控制并发度:
@Service
public class OrderService {
// 控制数据库访问并发度
private final Semaphore dbSemaphore = new Semaphore(200);
// 控制外部API调用并发度
private final Semaphore apiSemaphore = new Semaphore(100);
@Async
public CompletableFuture<OrderResult> processOrder(OrderRequest request) {
return CompletableFuture.supplyAsync(() -> {
try {
// 获取数据库访问许可
dbSemaphore.acquire();
try {
Order order = orderRepository.findByOrderNo(request.getOrderNo());
return processOrderInternal(order, request);
} finally {
dbSemaphore.release();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("订单处理被中断", e);
}
});
}
private OrderResult processOrderInternal(Order order, OrderRequest request) {
try {
apiSemaphore.acquire();
try {
PaymentResult paymentResult = paymentClient.processPayment(request);
order.setStatus(paymentResult.getStatus());
dbSemaphore.acquire();
try {
orderRepository.save(order);
} finally {
dbSemaphore.release();
}
return new OrderResult(order);
} finally {
apiSemaphore.release();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("支付处理被中断", e);
}
}
}
3. 增强监控体系
构建虚拟线程专用的监控,监控虚拟线程的活跃数量、执行时间等关键参数:
@Component
public class VirtualThreadMonitor {
private final MeterRegistry meterRegistry;
private final AtomicLong virtualThreadCount = new AtomicLong(0);
public VirtualThreadMonitor(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
setupMetrics();
}
private void setupMetrics() {
Gauge.builder("virtual.threads.active")
.description("活跃虚拟线程数量")
.register(meterRegistry, virtualThreadCount, AtomicLong::get);
}
public void trackVirtualThreadExecution(Runnable task) {
virtualThreadCount.incrementAndGet();
Timer.Sample sample = Timer.start(meterRegistry);
try {
task.run();
} finally {
virtualThreadCount.decrementAndGet();
sample.stop(Timer.builder("virtual.thread.execution.time")
.description("虚拟线程执行时间")
.register(meterRegistry));
}
}
}
4. 分级处理策略
根据任务重要性实施分级处理:
@Service
public class TieredOrderProcessor {
private final ExecutorService criticalExecutor =
Executors.newVirtualThreadPerTaskExecutor();
private final ExecutorService normalExecutor =
Executors.newVirtualThreadPerTaskExecutor();
private final Semaphore criticalSemaphore = new Semaphore(100);
private final Semaphore normalSemaphore = new Semaphore(300);
public CompletableFuture<OrderResult> processOrder(OrderRequest request) {
if (request.isPriority()) {
return processWithSemaphore(request, criticalExecutor, criticalSemaphore);
} else {
return processWithSemaphore(request, normalExecutor, normalSemaphore);
}
}
private CompletableFuture<OrderResult> processWithSemaphore(
OrderRequest request,
ExecutorService executor,
Semaphore semaphore) {
return CompletableFuture.supplyAsync(() -> {
try {
semaphore.acquire();
try {
return doProcessOrder(request);
} finally {
semaphore.release();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("处理被中断", e);
}
}, executor);
}
}
讲几个重要原则
1. 避免使用 synchronized
虚拟线程在遇到 synchronized 时会被固定到载体线程上,失去轻量级优势:
// ❌ 错误用法
public synchronized void badMethod() {
// 虚拟线程会被固定,无法发挥优势
}
// ✅ 推荐用法
private final ReentrantLock lock = new ReentrantLock();
public void goodMethod() {
lock.lock();
try {
// 业务逻辑
} finally {
lock.unlock();
}
}
2. 合理配置资源池
// 资源池配置指导原则
public class ResourcePoolGuidelines {
// 数据库连接池:考虑数据库本身的连接限制
public static final int DB_POOL_SIZE =
Math.min(DATABASE_MAX_CONNECTIONS * 0.8, EXPECTED_CONCURRENT_DB_OPERATIONS);
// HTTP 连接池:考虑目标服务的处理能力
public static final int HTTP_POOL_SIZE =
EXPECTED_CONCURRENT_HTTP_REQUESTS;
// 文件句柄:考虑操作系统限制
public static final int FILE_POOL_SIZE =
Math.min(OS_MAX_FILE_HANDLES * 0.6, EXPECTED_CONCURRENT_FILE_OPERATIONS);
}
3. 实施优雅降级
@Component
public class GracefulDegradationService {
private final CircuitBreaker circuitBreaker;
private final AtomicInteger activeVirtualThreads = new AtomicInteger(0);
private static final int MAX_VIRTUAL_THREADS = 10000;
public CompletableFuture<String> processWithDegradation(String request) {
// 检查系统负载
if (activeVirtualThreads.get() > MAX_VIRTUAL_THREADS) {
return CompletableFuture.completedFuture(
getFallbackResponse(request));
}
// 使用断路器保护
return circuitBreaker.executeSupplier(() -> {
activeVirtualThreads.incrementAndGet();
try {
return processRequest(request);
} finally {
activeVirtualThreads.decrementAndGet();
}
});
}
}
结尾
这次生产事故着实给我们带来了深刻的教训,又一次给我们上了一课——新技术永远不是银弹。
技术的进步必然伴随挑战,关键在于如何在求新和求稳之间找到平衡。
这次事故虽然带来了损失,但也让整体团队对新技术有了更加理性和深入的认识。
花钱买教训。
希望我们的经历能够为正在考虑使用虚拟线程的开发者提供一些参考,避免重蹈覆辙。