Java 25 虚拟线程文档更新后如何重新评估阻塞式服务
来源:17golang原创
时间:2026-09-14 21:54:42 493浏览 收藏
我最近重新看 Java 25 的虚拟线程文档时,最容易误判的一点不是 API,而是“线程池大小”这个旧指标。虚拟线程适合把大量等待网络、数据库或文件的任务写成直线代码,但它不会让一次 SQL 或一次 HTTP 调用本身变快。重新评估阻塞式服务时,应该把承载任务的线程数和保护下游资源的并发额度拆开。
如果服务的大部分时间都在等待,并且并发任务数较高,可以先把请求执行器改成每任务一个虚拟线程;数据库连接数、下游限额和 CPU 计算量仍要单独限制。迁移是否成功,要看吞吐、排队和资源利用率是否改善,而不是只看线程数量。
- 虚拟线程优化的是高并发等待型负载的吞吐,不是单请求延迟。
- 不要用虚拟线程池限制数据库或下游服务,应该用信号量、连接池和明确的配额。
- Java 25 的 JFR、线程转储和 VirtualThreadSchedulerMXBean 可以帮助确认迁移后的真实瓶颈。
先判断:阻塞是不是服务的主要工作
传统固定线程池把“平台线程很贵”和“下游最多允许多少并发”混在了一个数字里。例如线程池设为 200,可能只是因为机器无法长期承载更多平台线程,也可能是数据库连接池只有 200 个连接。这两个限制并不是一回事。
可以先按一次请求的时间拆分:CPU 执行占比、等待 HTTP 或 JDBC 的时间、排队等待连接的时间,以及序列化和日志时间。如果 CPU 已经接近核心数,虚拟线程不会凭空增加计算能力;如果请求大部分时间停在可阻塞的 I/O 上,增加可挂起的任务数才有意义。JEP 444 对这一点的表述很明确:虚拟线程追求的是 scale,而不是 speed。
| 观察到的现象 | 优先处理方式 | 不要误判为 |
|---|---|---|
| 大量请求等待网络,平台线程长期空闲或排队 | 尝试每任务一个虚拟线程 | 单次网络调用会变快 |
| CPU 持续满载,任务主要做压缩或计算 | 保持 CPU 并行度接近核心数 | 虚拟线程能替代计算线程池 |
| 连接池耗尽、下游返回限流 | 单独设置连接数和信号量 | 把虚拟线程数量改小就解决了资源边界 |
把任务执行器改成每任务一个虚拟线程
Java 25 API 文档中的 Executors.newVirtualThreadPerTaskExecutor() 会为每个提交的任务创建一个虚拟线程,它不是把虚拟线程放进传统意义上的共享线程池。对请求、批量小任务或扇出调用来说,最小改法是保留 ExecutorService 的提交和等待语义,只替换执行器的来源。
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
// 每个外部调用对应一个独立任务,不复用昂贵的平台线程
var user = executor.submit(() -> userClient.fetch(userId));
var orders = executor.submit(() -> orderClient.fetch(userId));
// 等待两个结果,异常沿着任务边界返回给请求处理代码
return new Profile(user.get(), orders.get());
} catch (InterruptedException e) {
// 保留中断标记,避免上层取消信号被吞掉
Thread.currentThread().interrupt();
throw new ServiceUnavailableException(e);
} catch (ExecutionException e) {
// 生产代码应按 cause 分类记录下游错误
throw new DownstreamException(e.getCause());
}
这里的关键不是把并发数调到很大,而是让一个请求在等待时释放 carrier。执行器用 try-with-resources 关闭时会等待已经提交的任务,因此特别适合一次请求内的有限扇出。任务本身仍要有超时、取消和异常分类,虚拟线程不会替你补上这些业务边界。

别再用线程池大小限制数据库和下游服务
迁移后最常见的回退是继续创建一个“虚拟线程池大小为 50”的包装器。这样既失去了大量挂起任务的伸缩空间,也没有表达真正的资源约束。更清楚的写法是:虚拟线程负责承载请求,信号量负责保护一个最多允许 50 个并发操作的下游。
private final Semaphore downstreamSlots = new Semaphore(50);
Result callDownstream(Request request) throws Exception {
downstreamSlots.acquire(); // 只限制下游额度,不限制虚拟线程总数
try {
return httpClient.send(request); // 阻塞等待时,虚拟线程可以让出 carrier
} finally {
downstreamSlots.release(); // 无论成功或失败都归还额度
}
}
数据库连接池、HTTP 客户端连接上限、消息系统配额和供应商限流都应有自己的指标。信号量也不是万能的:如果任务拿到许可后又做很长的 CPU 计算,应该缩小受保护区间;如果下游库已经内置连接池,则要确认外层信号量不会造成双重排队。
重新检查三个容易被忽略的边界
第一是 ThreadLocal。虚拟线程支持线程本地变量,但每个任务一个线程会改变“在线程池线程上缓存昂贵资源”的经济性。连接、会话或大对象不应因为换成虚拟线程而被每个请求各自复制,应该交给连接池、请求上下文或显式缓存。
第二是长时间阻塞的临界区和 native 调用。虚拟线程在某些场景下无法从 carrier 卸载;频繁且长时间的 pinning 会降低并发收益。JFR 的虚拟线程事件和 jdk.tracePinnedThreads 可以用来定位,而不是凭感觉把所有 synchronized 全部替换。
第三是观测方式。Java 25 提供的 VirtualThreadSchedulerMXBean 可以查看调度器目标并行度、carrier 数量和排队的虚拟线程数。迁移前后至少对比下表中的指标:
| 指标 | 要回答的问题 | 异常信号 |
|---|---|---|
| 请求吞吐与 p95/p99 | 等待型并发是否真正转化为更多完成请求 | 吞吐不变但尾延迟升高 |
| 数据库连接池等待 | 瓶颈是否已经转移到数据库 | 线程变多,连接等待更长 |
| 虚拟线程队列与 carrier 数 | 调度器是否积压任务 | 队列持续增长或 CPU 饱和 |
| JFR pinned 事件 | 是否有同步块或 native 调用卡住 carrier | 长时间、频繁出现 pinning |

常见问题
虚拟线程能替代所有固定线程池吗?
不能。它适合承载大量等待型任务;CPU 密集型工作、定时调度、批量资源配额和有界队列仍需要明确的并行度或背压设计。
迁移后还需要连接池吗?
需要。虚拟线程不是数据库连接,也不会增加数据库能处理的并发量。连接池大小应按数据库和业务压测结果设置。
看到 synchronized 就必须改成 ReentrantLock 吗?
不必。只保护短小内存操作或只在启动阶段执行的同步块通常不值得改;优先调查会在同步区内进行长时间等待的热点。
怎样判断这次迁移值得保留?
用同一流量、同一依赖配额比较吞吐、尾延迟、CPU、连接池等待、队列长度和错误率。如果只是线程数下降而用户结果没有改善,就还没有证明迁移有效。
-
430 收藏
-
446 收藏
-
226 收藏
-
241 收藏
-
146 收藏
-
科技周边 · 业界新闻 | 8小时前 | 云原生 · 调度器 · kubernetes · 资源调节 · Kubernetes v1.37 调度器抢占 InPlacePodVerticalScaling Pod resize122 收藏
-
科技周边 · 业界新闻 | 9小时前 | kubernetes · Gateway API · TCPRoute · 云原生网络 · 入口迁移 · Gateway API v1.6 TCPRoute v1迁移 Gateway入口规则评估219 收藏
-
科技周边 · 业界新闻 | 10小时前 | 云原生 · kubernetes · job · successPolicy · Kubernetes v1.37 Job successPolicy Indexed Job succeededIndexes succeededCount271 收藏
-
科技周边 · 业界新闻 | 12小时前 | kubernetes · 故障排查 · job · 业界新闻 · 容器编排 · Kubernetes v1.37 PodFailurePolicy Job FailureTarget Pod失败策略249 收藏
-
298 收藏
-
科技周边 · 业界新闻 | 1天前 | 云原生 · kubernetes · Gateway API · 网络迁移 · ingress 路由迁移 Gateway API Ingress2Gateway ingress-nginx335 收藏
-
195 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习