Java 中使用虚拟线程重构高并发 I/O 服务的实战指南
在 Java 服务里,只要业务是“高并发 + 大量等待 I/O”,就几乎绕不开一个老问题:线程很多,但 CPU 并不忙。
比如网关、聚合服务、通知分发、爬虫、外部 API 编排、数据库读多写少的查询接口,真正耗时的往往不是计算,而是:
- 等数据库返回
- 等 HTTP 接口响应
- 等缓存或消息系统
- 等文件、对象存储、RPC
这类场景下,传统线程池模型能跑,但写着写着就会遇到几个典型症状:
- 线程池参数越来越难调
- 队列积压,延迟抖动明显
- 代码里充满
CompletableFuture、回调和拼装逻辑 - 某些链路为了“提升吞吐”,被迫写成异步,但排查问题更难了
虚拟线程(Virtual Thread)的价值就在这里:让“一个请求一个线程”的直觉式写法重新变得可行,而且成本显著降低。
这篇文章我会从架构改造的角度,带你把一个典型高并发 I/O 服务,从“平台线程 + 线程池”思路,重构到“虚拟线程优先”的模型里。重点不是语法演示,而是:什么时候值得改、怎么改、怎么验证、哪些坑最容易踩。
背景与问题
先看一个典型服务:聚合查询服务。
它收到一个请求后,需要并发调用:
- 用户中心查用户信息
- 订单中心查最近订单
- 推荐服务查个性化推荐
- 风控服务查风险标签
如果用传统平台线程,常见写法大概有两类。
方案 A:同步阻塞 + 固定线程池
优点是简单,缺点是高并发时线程池很容易成为瓶颈。
方案 B:CompletableFuture / Reactor 异步编排
优点是线程利用率提升,缺点是:
- 业务逻辑被拆碎
- 异常处理分散
- 超时和取消传播不自然
- 上下文透传(日志 trace、MDC、租户信息)容易丢
很多团队走到这一步会发现一个现实:
我们不是不会写异步,而是异步写到一定规模后,维护成本开始吃掉性能收益。
虚拟线程出现后,给了第三种选择:
方案 C:同步风格代码 + 虚拟线程调度
对于 I/O 密集型服务,这通常是一个非常有吸引力的中间地带:
- 代码仍然像同步代码一样好读
- 并发数量可以远高于平台线程
- 阻塞等待 I/O 时,不必长期占用昂贵的 OS 线程
方案对比与适用边界
先把边界说清楚,避免“见到虚拟线程就全量替换”。
| 方案 | 编程模型 | 适合场景 | 优点 | 缺点 |
|---|---|---|---|---|
| 平台线程 + 线程池 | 阻塞式 | 并发不高、CPU 计算多 | 简单稳定 | 高并发 I/O 下线程成本高 |
CompletableFuture / 响应式 | 非阻塞式 | 极致吞吐、生态已异步化 | 资源利用率高 | 复杂度高,调试难 |
| 虚拟线程 | 阻塞式写法 + 轻量线程 | 高并发 I/O、链路编排 | 可读性好,迁移成本低 | 不适合盲目覆盖 CPU 密集任务 |
什么时候优先考虑虚拟线程?
比较适合:
- 请求大部分时间在等 I/O
- 现有代码是同步风格,改成异步成本高
- 服务端需要承载大量并发连接或短请求
- 团队更看重可维护性和定位效率
不太适合一把梭:
- 核心逻辑是 CPU 密集计算
- 大量使用 native 调用或第三方库,可能有 pinning 风险
- 业务依赖的中间件驱动并不成熟,阻塞行为不可控
- 系统瓶颈其实在数据库、下游限流,而不是线程模型
一句话总结:
虚拟线程主要解决“等待太多、线程太贵、异步太复杂”的问题,不是万能性能开关。
核心原理
1. 虚拟线程到底是什么
虚拟线程是由 JVM 管理的轻量级线程。
你可以把它理解成:应用代码仍然按 Thread 的方式写,但真正运行时由 JVM 调度到少量平台线程上执行。
当虚拟线程执行到阻塞型 I/O 操作时,JVM 可以把它“挂起”,让底层平台线程去跑别的任务;等 I/O 完成后,再恢复这个虚拟线程继续执行。
这和“一个请求绑定一个 OS 线程”相比,成本低很多。
flowchart LR
A[请求进入] --> B[创建虚拟线程]
B --> C[执行同步业务代码]
C --> D{遇到 I/O 阻塞?}
D -- 否 --> E[继续运行]
D -- 是 --> F[虚拟线程挂起]
F --> G[平台线程释放去执行其他任务]
G --> H[I/O 完成]
H --> I[恢复虚拟线程]
I --> J[返回响应]
2. 调度模型:Carrier Thread
虚拟线程不是直接跑在空气里,它最终还是要映射到平台线程上,这些底层承载执行的线程通常被称为 carrier threads。
关键点在于:
- 虚拟线程数量可以非常多
- 平台线程数量相对少
- 阻塞等待时,虚拟线程可以卸载
- 恢复执行时,再被调度到某个平台线程
所以对于 I/O 密集型场景,吞吐通常会比“固定大小线程池 + 长时间阻塞”更健康。
3. 为什么它特别适合 I/O 服务
因为 I/O 服务最常见的问题不是算不过来,而是等得太久。
传统线程模型下,线程在等网络返回时,也占着一个 OS 线程;虚拟线程模型下,这个占用可以大幅减轻。
4. 但它不是“自动非阻塞”
这个点很重要,我见过不少误解:
- 虚拟线程不是把阻塞代码 magically 变成异步代码
- 它只是让“阻塞等待”变得没那么昂贵
- 如果你的下游本身慢、连接池太小、数据库顶不住,虚拟线程不会替你解决容量问题
一次典型重构:从线程池编排到虚拟线程
下面我们用一个可运行示例来演示。
场景是一个“用户主页聚合服务”,会并发请求三个外部接口:
- 用户资料
- 订单列表
- 推荐结果
为了方便本地运行,我直接用 JDK 自带的 HttpServer 模拟三个慢接口,再分别实现:
- 传统固定线程池版本
- 虚拟线程版本
运行要求:建议 JDK 21+
实战代码(可运行)
示例结构说明
/profile?id=1:模拟 300ms/orders?id=1:模拟 500ms/recommend?id=1:模拟 400ms/aggregate-platform?id=1:平台线程池聚合/aggregate-virtual?id=1:虚拟线程聚合
完整示例代码
import com.sun.net.httpserver.HttpServer;
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.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.concurrent.*;
public class VirtualThreadIoDemo {
private static final HttpClient CLIENT = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(2))
.build();
private static final ExecutorService PLATFORM_POOL =
Executors.newFixedThreadPool(50);
public static void main(String[] args) throws Exception {
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
// 模拟下游 I/O 接口
server.createContext("/profile", exchange -> {
sleep(300);
writeJson(exchange, "{\"name\":\"Alice\",\"id\":1}");
});
server.createContext("/orders", exchange -> {
sleep(500);
writeJson(exchange, "{\"orders\":[101,102,103]}");
});
server.createContext("/recommend", exchange -> {
sleep(400);
writeJson(exchange, "{\"items\":[\"book\",\"keyboard\"]}");
});
// 传统平台线程池聚合
server.createContext("/aggregate-platform", exchange -> {
String id = getQueryParam(exchange.getRequestURI(), "id", "1");
long start = System.currentTimeMillis();
try {
String result = aggregateWithPlatformPool(id);
long cost = System.currentTimeMillis() - start;
writeJson(exchange, "{\"mode\":\"platform\",\"costMs\":" + cost + ",\"data\":" + result + "}");
} catch (Exception e) {
writeJson(exchange, "{\"error\":\"" + e.getMessage() + "\"}", 500);
}
});
// 虚拟线程聚合
server.createContext("/aggregate-virtual", exchange -> {
String id = getQueryParam(exchange.getRequestURI(), "id", "1");
long start = System.currentTimeMillis();
try {
String result = aggregateWithVirtualThreads(id);
long cost = System.currentTimeMillis() - start;
writeJson(exchange, "{\"mode\":\"virtual\",\"costMs\":" + cost + ",\"data\":" + result + "}");
} catch (Exception e) {
writeJson(exchange, "{\"error\":\"" + e.getMessage() + "\"}", 500);
}
});
server.setExecutor(Executors.newCachedThreadPool());
server.start();
System.out.println("Server started at http://localhost:8080");
}
private static String aggregateWithPlatformPool(String id) throws Exception {
Future<String> profileFuture = PLATFORM_POOL.submit(() -> fetch("http://localhost:8080/profile?id=" + id));
Future<String> ordersFuture = PLATFORM_POOL.submit(() -> fetch("http://localhost:8080/orders?id=" + id));
Future<String> recommendFuture = PLATFORM_POOL.submit(() -> fetch("http://localhost:8080/recommend?id=" + id));
String profile = profileFuture.get(2, TimeUnit.SECONDS);
String orders = ordersFuture.get(2, TimeUnit.SECONDS);
String recommend = recommendFuture.get(2, TimeUnit.SECONDS);
return """
{
"profile": %s,
"orders": %s,
"recommend": %s
}
""".formatted(profile, orders, recommend);
}
private static String aggregateWithVirtualThreads(String id) throws Exception {
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
Future<String> profileFuture = executor.submit(() -> fetch("http://localhost:8080/profile?id=" + id));
Future<String> ordersFuture = executor.submit(() -> fetch("http://localhost:8080/orders?id=" + id));
Future<String> recommendFuture = executor.submit(() -> fetch("http://localhost:8080/recommend?id=" + id));
String profile = profileFuture.get(2, TimeUnit.SECONDS);
String orders = ordersFuture.get(2, TimeUnit.SECONDS);
String recommend = recommendFuture.get(2, TimeUnit.SECONDS);
return """
{
"profile": %s,
"orders": %s,
"recommend": %s
}
""".formatted(profile, orders, recommend);
}
}
private static String fetch(String url) throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.timeout(Duration.ofSeconds(2))
.GET()
.build();
return CLIENT.send(request, HttpResponse.BodyHandlers.ofString()).body();
}
private static void writeJson(com.sun.net.httpserver.HttpExchange exchange, String body) throws IOException {
writeJson(exchange, body, 200);
}
private static void writeJson(com.sun.net.httpserver.HttpExchange exchange, String body, int status) throws IOException {
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().add("Content-Type", "application/json; charset=UTF-8");
exchange.sendResponseHeaders(status, bytes.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(bytes);
}
}
private static String getQueryParam(URI uri, String key, String defaultValue) {
String query = uri.getQuery();
if (query == null || query.isBlank()) return defaultValue;
String[] pairs = query.split("&");
for (String pair : pairs) {
String[] kv = pair.split("=", 2);
if (kv.length == 2 && kv[0].equals(key)) {
return kv[1];
}
}
return defaultValue;
}
private static void sleep(long ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
如何运行与验证
编译运行
javac VirtualThreadIoDemo.java
java VirtualThreadIoDemo
浏览器或 curl 测试
curl "http://localhost:8080/aggregate-platform?id=1"
curl "http://localhost:8080/aggregate-virtual?id=1"
你会发现单次请求的耗时都接近最长的那个下游调用,大约 500ms 左右,因为三个请求是并发发出的。
真正差异出现在高并发压力下。
简单压测思路
如果你有 wrk 或 ab,可以这样压:
wrk -t4 -c200 -d30s "http://localhost:8080/aggregate-platform?id=1"
wrk -t4 -c200 -d30s "http://localhost:8080/aggregate-virtual?id=1"
如果平台线程池大小偏小,aggregate-platform 很容易在高并发下出现:
- 队列堆积
- p99 延迟上升
- 超时增加
而虚拟线程版本通常会更平滑,尤其是在“任务数远大于平台线程数”的等待型场景下更明显。
请求处理链路对比
sequenceDiagram
participant C as Client
participant A as AggregateService
participant P as ProfileService
participant O as OrderService
participant R as RecommendService
C->>A: 请求用户主页
par 并发获取资料
A->>P: GET /profile
P-->>A: profile
and 并发获取订单
A->>O: GET /orders
O-->>A: orders
and 并发获取推荐
A->>R: GET /recommend
R-->>A: recommend
end
A-->>C: 聚合结果
架构层面的重构建议
落地时,我不建议直接“全服务替换”,更稳妥的做法是分层推进。
第一层:先把 I/O 编排改成虚拟线程
最有收益的是这些地方:
- 多下游聚合调用
- 批量任务处理
- 阻塞式 DAO / 客户端调用
- 每个请求都要发起多个远程访问的链路
这一步往往能减少很多 CompletableFuture 拼装代码。
第二层:保留必要的资源边界
虚拟线程很轻,但下游资源不是无限的。
比如数据库连接池只有 100 个,你起 5 万个虚拟线程一起查库,不会创造奇迹,只会更快把连接池打满。
所以架构上要明确两类边界:
- 线程边界:虚拟线程可以放宽
- 资源边界:连接池、限流、舱壁隔离必须保留
第三层:逐步观测,再决定是否扩大范围
先从最典型的 I/O 场景试点:
- 聚合查询
- 第三方 API 编排
- 报表导出
- 消息消费中的外部调用
不要一开始就改到核心交易链路最深处。
容量估算:别只看线程数
很多团队一上虚拟线程,最容易兴奋的点是:
“能开几十万线程了!”
这话技术上没错,但架构上不够完整。你应该更关注:
1. 下游连接池容量
例如:
- HTTP 客户端连接池:200
- 数据库连接池:50
- Redis 连接池:100
即使你能创建 10 万虚拟线程,真正能同时访问下游的仍然受连接池限制。
2. 超时配置
高并发 I/O 服务如果没有严格超时,虚拟线程只会让更多请求“优雅地一起卡住”。
建议按层次设置:
- connect timeout
- read timeout / request timeout
- 聚合总超时
- 重试上限
3. 内存占用
虚拟线程比平台线程便宜很多,但不是零成本。
如果你让大量请求无限挂起,内存照样会涨。
一个实用估算思路
假设:
- 峰值并发请求数:5000
- 每个请求并发调用 3 个外部服务
- 平均 I/O 等待 300ms
- 聚合超时 1s
你要重点检查的是:
- 下游是否允许这么多并发调用
- HTTP 连接池是否足够
- 是否需要按下游做并发信号量限制
- 失败和超时是否会快速释放资源
常见坑与排查
这一节我尽量写得“接地气”一点,因为真实项目里,问题往往不是出在 Executors.newVirtualThreadPerTaskExecutor() 这一行,而是周边生态。
坑 1:把虚拟线程当成“无限并发许可证”
表现:
- QPS 上去了,但数据库雪崩
- 下游 429 / 503 变多
- 线程没爆,连接池先爆了
原因:
虚拟线程降低的是线程成本,不是下游资源成本。
建议:
- 对数据库、HTTP 下游加并发上限
- 用 semaphore 或 bulkhead 模式做隔离
- 明确每类资源的最大并发访问数
示例:
import java.util.concurrent.Semaphore;
public class DownstreamGuard {
private final Semaphore semaphore = new Semaphore(100);
public <T> T call(CallableWithException<T> action) throws Exception {
semaphore.acquire();
try {
return action.call();
} finally {
semaphore.release();
}
}
@FunctionalInterface
interface CallableWithException<T> {
T call() throws Exception;
}
}
坑 2:线程局部变量(ThreadLocal)滥用
很多老项目里都有:
- 用户上下文
- TraceId
- 数据源路由信息
- 灰度标记
它们常常放在 ThreadLocal 里。
虚拟线程本身支持 ThreadLocal,但如果你在大量短生命周期虚拟线程里频繁塞大对象,可能会让上下文管理变得混乱,也增加内存压力。
建议:
ThreadLocal只存轻量信息- 请求结束后及时清理
- 能显式传参就别全靠隐式上下文
坑 3:Pinning 导致载体线程被“钉住”
这是虚拟线程里一个非常值得关注的问题。
所谓 pinning,简单说就是:
某些操作会让虚拟线程无法从 carrier thread 上卸载,从而失去“轻量阻塞”的优势。
高风险场景包括:
- 长时间持有
synchronized - 某些 native / foreign 调用
- 一些第三方库内部实现不友好
如果在阻塞 I/O 期间又发生 pinning,底层平台线程可能被长期占住。
排查建议:
- 尽量避免大范围
synchronized包住 I/O - 优先用
ReentrantLock - 压测时关注线程 dump 和 JVM 诊断信息
- 使用 JFR 观察虚拟线程相关事件
错误示例:
public class BadService {
public synchronized String callSlowApi() throws Exception {
Thread.sleep(500); // 模拟阻塞等待
return "ok";
}
}
更稳妥的方式:
import java.util.concurrent.locks.ReentrantLock;
public class BetterService {
private final ReentrantLock lock = new ReentrantLock();
public String callSlowApi() throws Exception {
lock.lock();
try {
// 这里只保护共享状态更新,尽量不要把慢 I/O 放进锁里
} finally {
lock.unlock();
}
Thread.sleep(500);
return "ok";
}
}
坑 4:取消和超时没做好
虚拟线程让你更容易写出同步风格代码,但如果:
- 不设超时
- 不处理中断
- 失败后还继续等待其他下游
最终会出现“看起来很优雅,实际上挂得很整齐”的情况。
建议:
- 每个下游调用都设超时
- 聚合服务设总超时
- 捕获
InterruptedException后及时恢复中断标记 - 超时后尽早返回降级结果
坑 5:框架版本不匹配
如果你是在 Spring Boot 或其他框架里落地,别只看 JDK 版本,还要看:
- Web 容器是否支持良好
- JDBC 驱动版本是否稳定
- HTTP 客户端是否有已知问题
- 监控工具是否认识虚拟线程
这个坑我自己踩过:
本地 Demo 很顺,线上一接旧版本组件,线程指标和日志链路立刻变得很怪。
所以试点时一定要做一套最小闭环验证,不要只跑单元测试。
典型排查路径
如果你上线后发现“虚拟线程效果不明显”或者“吞吐提高了但延迟变差”,可以按下面这个顺序查。
flowchart TD
A[高并发下性能异常] --> B{CPU 是否打满?}
B -- 是 --> C[更像 CPU 瓶颈,不是虚拟线程主战场]
B -- 否 --> D{下游连接池是否耗尽?}
D -- 是 --> E[扩容/限流/舱壁隔离]
D -- 否 --> F{是否存在大量超时与重试?}
F -- 是 --> G[收紧超时策略,减少级联放大]
F -- 否 --> H{是否有 pinning 或锁竞争?}
H -- 是 --> I[检查 synchronized/native 调用/JFR]
H -- 否 --> J[检查框架兼容性与监控误差]
建议重点观察的指标
- 请求吞吐量(QPS)
- 平均延迟 / p95 / p99
- 活跃请求数
- 下游连接池使用率
- 超时率、取消率、重试率
- JVM 堆内存与 GC 次数
- 平台线程数量与阻塞栈
安全/性能最佳实践
这一部分我按“能直接落地”的方式列出来。
1. 只在 I/O 密集路径优先使用虚拟线程
优先级高的场景:
- HTTP 聚合调用
- JDBC 查询密集接口
- 文件/对象存储访问
- 消费消息后调用外部系统
不建议为了“统一风格”把纯 CPU 计算也都包成海量虚拟线程。
2. 保留背压与限流机制
虚拟线程不是背压机制。
你仍然需要:
- 网关限流
- 服务级并发上限
- 下游舱壁隔离
- 熔断与降级
尤其是“一个入口请求会放大成多个下游请求”的服务,最怕无边界扩散。
3. 明确超时分层
建议至少分三层:
- 单次下游请求超时
- 聚合总超时
- 客户端可感知超时
例如:
- profile:300ms
- orders:500ms
- recommend:400ms
- aggregate 总超时:800ms
如果总超时已经到了,就不要再傻等最慢分支。
4. 控制锁粒度,避免阻塞区持锁
这一条在虚拟线程时代比过去更重要。
经验上:
- 共享状态更新时再加锁
- 锁内不要做网络 I/O
- 能拆分临界区就拆分
- 能用无锁结构就别上大锁
5. 优先选择成熟客户端与新版本驱动
不要一边上虚拟线程,一边用多年没升级的客户端库。
尤其关注:
- JDBC 驱动
- HTTP 客户端
- Redis 客户端
- 监控探针
- APM Agent
有些问题不是虚拟线程本身,而是周边工具链还没跟上。
6. 压测要模拟真实等待,而不是只压空接口
很多人压测时只打一个几乎不做事的 endpoint,然后得出“虚拟线程没啥提升”的结论。
这不奇怪,因为它的优势主要体现在等待 I/O时。
压测要尽量贴近真实:
- 多分支并发下游
- 真正的超时和失败比例
- 有限的连接池
- 有波峰波谷
- 观察长尾延迟
7. 做灰度,不要梭哈
我的建议是:
- 先选一个 I/O 聚合接口试点
- 与现网线程池版本并行
- 比较一周核心指标
- 再扩大到相似链路
如果你一上来就全站替换,回滚和归因都会非常痛苦。
一种更贴近生产的组织方式
如果你用的是分层架构,可以参考下面的职责划分:
classDiagram
class Controller {
+handleRequest()
}
class AggregationService {
+aggregateUserHome()
}
class ProfileClient {
+fetchProfile()
}
class OrderClient {
+fetchOrders()
}
class RecommendClient {
+fetchRecommend()
}
class BulkheadGuard {
+call()
}
Controller --> AggregationService
AggregationService --> ProfileClient
AggregationService --> OrderClient
AggregationService --> RecommendClient
AggregationService --> BulkheadGuard
这里的关键点是:
- Controller 不关心线程模型
- AggregationService 用虚拟线程组织并发
- 每个 Client 负责超时、重试、异常翻译
- BulkheadGuard 负责资源隔离
这样改造的好处是:虚拟线程只是执行策略,不会污染整个业务模型。
一个更像生产代码的聚合写法
如果你希望代码更清晰一点,可以这样封装:
import java.util.concurrent.*;
public class UserHomeAggregationService {
public String aggregate(String id) throws Exception {
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
Future<String> profile = executor.submit(() -> fetchProfile(id));
Future<String> orders = executor.submit(() -> fetchOrders(id));
Future<String> recommend = executor.submit(() -> fetchRecommend(id));
try {
return """
{
"profile": %s,
"orders": %s,
"recommend": %s
}
""".formatted(
profile.get(800, TimeUnit.MILLISECONDS),
orders.get(800, TimeUnit.MILLISECONDS),
recommend.get(800, TimeUnit.MILLISECONDS)
);
} catch (TimeoutException e) {
profile.cancel(true);
orders.cancel(true);
recommend.cancel(true);
throw new RuntimeException("aggregate timeout", e);
}
}
}
private String fetchProfile(String id) throws InterruptedException {
Thread.sleep(300);
return "{\"id\":%s,\"name\":\"Alice\"}".formatted(id);
}
private String fetchOrders(String id) throws InterruptedException {
Thread.sleep(500);
return "{\"orders\":[1,2,3]}";
}
private String fetchRecommend(String id) throws InterruptedException {
Thread.sleep(400);
return "{\"items\":[\"book\",\"mouse\"]}";
}
}
这个版本体现了几个很重要的实践:
- 结构仍然是同步风格
- 并发逻辑很好理解
- 总超时明确
- 超时后会取消其他子任务
总结
如果你现在维护的是一个高并发 I/O 服务,而且团队正被这些问题困扰:
- 线程池调参越来越玄学
- 异步编排代码越来越难维护
- 高峰期延迟抖动严重
- 大量时间都耗在等待下游
那么虚拟线程非常值得认真评估。
我对它的建议不是“全面替代一切”,而是:
- 先识别 I/O 密集链路
- 优先重构聚合式、编排式服务
- 保留资源边界、超时、限流、隔离
- 重点排查 pinning、锁竞争、驱动兼容
- 用真实压测和灰度数据说话
最后给一个务实结论:
- 如果你的问题本质上是“等待太多”,虚拟线程通常很有帮助
- 如果你的问题本质上是“CPU 不够”或“下游扛不住”,虚拟线程不是解药
- 最佳落地方式往往不是“重写成响应式”,而是在同步代码可读性和高并发吞吐之间,找到一个更舒服的平衡点
这也是我认为虚拟线程最有价值的地方:
它不只是提升性能,更是在很多 I/O 场景里,把 Java 并发编程重新拉回了“人能轻松读懂”的范式。