登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  java教程

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

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删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>