当前位置:首页 > 文章列表 > 文章 > java教程 > Java 虚拟线程执行阻塞 IO 时怎么设置并发边界

Java 虚拟线程执行阻塞 IO 时怎么设置并发边界

来源:17golang原创 2026-09-07 00:52:04 0浏览 收藏

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。多个资源同时被占用时,要为每个资源分别设边界,或者在最窄的入口统一限流。

Java 虚拟线程执行器、Semaphore、阻塞 IO、连接池和外部服务的并发边界结构图
图1:任务承载、并发闸门和外部资源是三层不同边界,Semaphore 只控制进入受限阻塞 IO 的数量。

并发边界按哪个数定

不要从“虚拟线程最多开多少”开始调参,而要先列出一次调用会占用的资源。假设一个任务会占用一个数据库连接,数据库池上限为 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 辅助定位。它们是排查载体线程被占住的证据,不是替代资源限流的手段。

Java 并发边界由 Semaphore 许可、数据库连接池、上游 HTTP 限额和 CPU 预算共同决定的静态关系图
图2:有效并发边界同时受外部资源和本地预算约束,最终选择应落在最窄且可观测的限制上。

用四类指标迭代 permits

上线后至少记录四类数据:许可等待时长、取得许可后的 IO 时长、外部超时或拒绝数量、当前任务取消数量。若等待变长但外部 IO 很快,说明许可太小或请求突增;若 IO 本身变慢并伴随 429、连接耗尽或数据库排队,继续增加许可只会把压力推给依赖方。

调参时一次只改变一个资源的许可数,保留成功率、P95/P99 延迟和连接池使用率。还要确认异常路径释放许可,应用关闭时让执行器进入有序关闭,避免把“任务没完成”误判成“资源边界太小”。

常见问题

虚拟线程很多,会不会把 Semaphore 也撑爆?

Semaphore 本身只保存许可和等待者,真正需要控制的是等待时间与任务总量。可以给许可等待设置超时、对入口做背压,并限制单个请求能够提交的任务数量。

能不能直接用固定线程池代替 Semaphore?

固定线程池确实能限制同时执行的任务数,但它同时改变了任务调度和资源边界。虚拟线程场景下更清晰的做法是让每个任务拥有虚拟线程,再对数据库、HTTP 或其他稀缺资源分别使用专门的限制器。

许可数应该等于连接池大小吗?

它最多不应超过连接池的可用上限,但未必必须相等。还要扣除其他业务占用、事务持有时间和故障余量;最终用连接池排队、超时与吞吐数据验证。

收尾检查

设置 Java 虚拟线程阻塞 IO 的并发边界时,先用每任务一个虚拟线程承载请求,再把 Semaphore 放到真正稀缺资源的入口。许可数从连接池和上游硬上限推导,按等待、延迟、拒绝和 pinning 指标迭代;这样扩展的是任务吞吐,守住的仍是外部系统边界。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go 怎么用 io.Reader 统计上传进度而不复制文件Go 怎么用 io.Reader 统计上传进度而不复制文件
上一篇
Go 怎么用 io.Reader 统计上传进度而不复制文件
Go 切片 append 后原切片数据为什么也变了
下一篇
Go 切片 append 后原切片数据为什么也变了
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • SuperCLUE中文大模型评测基准:功能、能力维度与应用指南
    SuperCLUE
    SuperCLUE是权威的中文大语言模型综合评测基准,涵盖语言理解、知识应用、AI Agent智能体及安全性等12项核心能力。通过多轮对话与客观测试,定期发布榜单与技术报告,为模型研发、优化及行业选型提供科学依据。
    167次使用
  • C-Eval中文评测基准:大语言模型多学科能力评估指南
    C-Eval
    深入了解C-Eval中文评估套件,涵盖52个学科与4级难度。本文详解其功能特点、Zero-shot/Few-shot使用方法及代码示例,助您全面评测LLM中文理解与泛化能力。
    93次使用
  • AI Prompt Library:免费AI提示词库,助力ChatGPT高效创作与营销
    AI Prompt Library
    探索AI Prompt Library免费资源库,涵盖营销、写作及多场景AI提示词。兼容ChatGPT、Claude等工具,一键复制优化输出,提升工作效率。
    15次使用
  • LangGPT提示词框架:结构化Prompt设计方法与开源工具指南
    LangGPT
    LangGPT是一种受编程语言启发的结构化提示词设计工具,提供双层框架、模块化模板及变量功能,帮助用户高效编写高质量Prompt。该项目已在GitHub免费开源,适用于内容创作、编程辅助等多场景。
    28次使用
  • ClickPrompt:AI提示词生成与优化工具,支持Stable Diffusion、ChatGPT及代码辅助
    ClickPrompt
    ClickPrompt是一款专为AI提示词编写者设计的开源在线工具,支持Stable Diffusion绘图、ChatGPT对话及GitHub Copilot代码辅助。提供Prompt自动生成、一键运行、社区分享及可视化优化功能,帮助用户高效获取精准AI输出。
    61次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码