Java 虚拟线程执行阻塞 IO 时怎么设置并发边界
来源:17golang原创
时间:2026-09-07 00:52:04 415浏览 收藏
Java 虚拟线程执行阻塞 IO 时,通常不需要把虚拟线程数量硬限制成一个很小的线程池大小。更稳妥的分工是:用 Executors.newVirtualThreadPerTaskExecutor() 为每个任务创建虚拟线程,再用 Semaphore、数据库连接池或 HTTP 客户端连接上限限制真正稀缺的资源。
并发边界应贴着资源设置,而不是贴着虚拟线程设置。虚拟线程可以很多,但同时占用数据库连接、上游配额或本地句柄的任务必须有明确上限。
- 虚拟线程负责承载阻塞任务,不等于资源池,也不建议再把它们放进固定大小线程池。
Semaphore的许可数应参考连接池容量、上游限额和本地安全余量,取最小的有效边界。- 许可必须紧贴受限 IO 获取并在
finally中释放,同时观察等待时间、超时和 pinning。
先把两个并发数字分开
一个请求可能只需要创建一个虚拟线程,但它在访问数据库、调用外部 HTTP 服务时,还会占用连接、服务端并发槽位或文件句柄。前者是任务承载数,后者才是资源并发数。把这两个数字混成一个固定线程池大小,容易出现两种反效果:线程池太小,任务在本地排队;线程池太大,外部依赖被瞬间打满。
| 对象 | 它限制什么 | 典型做法 |
|---|---|---|
| 虚拟线程执行器 | 任务如何被承载 | 每任务一个虚拟线程 |
| Semaphore | 某段受限代码的同时进入数 | 许可数对应外部资源预算 |
| 连接池 | 实际可用连接数 | 以池容量作为硬约束 |
| CPU 预算 | 本地计算并行度 | 单独评估 CPU 密集阶段 |
因此,虚拟线程多不代表数据库连接也要多。JDK 21 的虚拟线程 API 适合高并发、等待时间较长的任务;它解决的是线程成本,不会扩大外部系统的承载能力。
用 Semaphore 包住真正受限的阻塞 IO
下面的写法把许可申请放在调用外部服务之前,把释放放在 finally 中。执行器不承担资源限流职责,Semaphore 才是这段 IO 的并发闸门。
import java.time.Duration;
import java.util.concurrent.*;
static final Semaphore HTTP_PERMITS = new Semaphore(32);
static String fetch(BlockingHttpClient client, String url)
throws InterruptedException, TimeoutException {
// 许可数对应上游服务和本地连接配置的共同上限
if (!HTTP_PERMITS.tryAcquire(2, TimeUnit.SECONDS)) {
throw new TimeoutException("等待 HTTP 并发许可超时");
}
try {
// 这里可以是同步阻塞调用,虚拟线程会承载等待过程
return client.get(url, Duration.ofSeconds(5));
} finally {
// 无论成功、超时还是业务异常,都必须归还许可
HTTP_PERMITS.release();
}
}
static void submitAll(BlockingHttpClient client, List urls)
throws InterruptedException {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (String url : urls) {
executor.submit(() -> {
try {
fetch(client, url);
} catch (TimeoutException e) {
// 超过等待预算时记录并交给上层重试策略
System.err.println("限流等待超时: " + url);
} catch (InterruptedException e) {
// 保留中断信号,避免任务取消被吞掉
Thread.currentThread().interrupt();
}
});
}
}
}
示例中的 BlockingHttpClient 是项目自己的同步客户端接口,重点不在客户端名称,而在许可边界。若数据库连接池只有 20 个连接,就不能因为虚拟线程很轻而把数据库查询许可设置成 200。多个资源同时被占用时,要为每个资源分别设边界,或者在最窄的入口统一限流。

并发边界按哪个数定
不要从“虚拟线程最多开多少”开始调参,而要先列出一次调用会占用的资源。假设一个任务会占用一个数据库连接,数据库池上限为 20;上游 HTTP 服务允许 50 个并发;本机 CPU 还有余量,那么数据库就是更窄的边界,数据库段的初始许可数应不超过 20,通常还要留出健康检查、管理请求和突发误差的余量。
如果一个任务先查库再调 HTTP,两个阶段不要共用一个过大的许可池。查库时取得数据库许可,查询返回后立即释放;调用 HTTP 时再取得 HTTP 许可。这样不会让一个正在等待上游响应的任务长期占着数据库连接。
| 判断项 | 问题 | 调整信号 |
|---|---|---|
| 资源容量 | 连接池、文件句柄或上游并发上限是多少? | 许可数不能超过硬上限 |
| 等待时间 | 许可等待是否越来越长? | 先查资源慢,不要只加 permits |
| 失败率 | 超时、拒绝或 429 是否随并发上升? | 降低边界并增加退避 |
| CPU 与内存 | 取得许可后的计算是否很重? | 把 CPU 密集段另行限并发 |
这个数没有脱离环境的通用答案。先用连接池和上游明确限制给出上界,再以等待时间、外部延迟和错误率做小步调整;不要拿虚拟线程数量本身当成压测目标。
阻塞 IO 和 pinning 不是同一件事
普通的 JDK 阻塞 IO 通常允许虚拟线程在等待期间卸载载体线程,所以“代码里出现阻塞调用”并不自动意味着设计错误。真正需要排查的是阻塞调用是否位于长时间、频繁进入的 synchronized 区域,或是否进入 native/foreign 调用导致虚拟线程 pinning。
如果锁只保护很短的内存操作,没必要为了虚拟线程迁移而全部改写;如果锁包住了远程 IO,则应缩小临界区,必要时评估 ReentrantLock。JDK Flight Recorder 可以观察 jdk.VirtualThreadPinned,启动阶段也可以用 -Djdk.tracePinnedThreads=short 辅助定位。它们是排查载体线程被占住的证据,不是替代资源限流的手段。

用四类指标迭代 permits
上线后至少记录四类数据:许可等待时长、取得许可后的 IO 时长、外部超时或拒绝数量、当前任务取消数量。若等待变长但外部 IO 很快,说明许可太小或请求突增;若 IO 本身变慢并伴随 429、连接耗尽或数据库排队,继续增加许可只会把压力推给依赖方。
调参时一次只改变一个资源的许可数,保留成功率、P95/P99 延迟和连接池使用率。还要确认异常路径释放许可,应用关闭时让执行器进入有序关闭,避免把“任务没完成”误判成“资源边界太小”。
常见问题
虚拟线程很多,会不会把 Semaphore 也撑爆?
Semaphore 本身只保存许可和等待者,真正需要控制的是等待时间与任务总量。可以给许可等待设置超时、对入口做背压,并限制单个请求能够提交的任务数量。
能不能直接用固定线程池代替 Semaphore?
固定线程池确实能限制同时执行的任务数,但它同时改变了任务调度和资源边界。虚拟线程场景下更清晰的做法是让每个任务拥有虚拟线程,再对数据库、HTTP 或其他稀缺资源分别使用专门的限制器。
许可数应该等于连接池大小吗?
它最多不应超过连接池的可用上限,但未必必须相等。还要扣除其他业务占用、事务持有时间和故障余量;最终用连接池排队、超时与吞吐数据验证。
收尾检查
设置 Java 虚拟线程阻塞 IO 的并发边界时,先用每任务一个虚拟线程承载请求,再把 Semaphore 放到真正稀缺资源的入口。许可数从连接池和上游硬上限推导,按等待、延迟、拒绝和 pinning 指标迭代;这样扩展的是任务吞吐,守住的仍是外部系统边界。
-
207 收藏
-
285 收藏
-
484 收藏
-
241 收藏
-
232 收藏
-
366 收藏
-
文章 · java教程 | 15小时前 | Java教程 · 空值处理 · Comparator · 集合排序 · java comparator 多字段排序 nullsFirst nullsLast310 收藏
-
351 收藏
-
311 收藏
-
188 收藏
-
136 收藏
-
116 收藏
-
114 收藏
-
382 收藏
-
443 收藏
-
452 收藏
-
456 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习