Java虚拟线程承载阻塞I/O服务的线程模型设计
来源:17golang原创
时间:2026-09-20 08:31:38 451浏览 收藏
Java 虚拟线程适合承载“请求很多、每个请求大部分时间都在等待”的服务。把一个请求放进 Executors.newVirtualThreadPerTaskExecutor() 后,数据库查询、HTTP 调用或 socket 读取发生阻塞时,运行时可以挂起虚拟线程并释放承载它的操作系统线程;但 CPU 密集计算不会因此变快,连接池、下游限流和内存也不会自动扩大。
- 使用 JDK 21 及以上,把虚拟线程按任务创建,不要把它们再放进固定大小线程池。
- 虚拟线程数量与数据库连接数、HTTP 并发额度是两件事,有限资源要用信号量或资源池单独限流。
- 排序、压缩、加密等 CPU 密集工作应交给有界的平台线程执行器;迁移后重点观察 pinning、队列长度和下游耗时。
实际落地时可以把请求处理保留为同步写法,把“每个请求一个线程”换成“每个任务一个虚拟线程”;只在等待型 I/O 周围放宽并发,在 CPU 计算和外部有限资源处设置独立边界。
先判断请求是在等待,还是在计算
虚拟线程的价值是提高并发承载量,不是让某一段 Java 代码跑得更快。一个请求如果大部分时间在等待数据库、HTTP、文件或队列,传统平台线程会长期占住操作系统线程;虚拟线程则可以在 JDK 支持的阻塞操作处暂停,把承载线程交给其他任务。相反,视频转码、复杂排序、密码计算等 CPU 密集工作会持续占用处理器,创建更多线程只会增加调度和上下文压力。

用按任务创建的虚拟线程承载阻塞 I/O
JDK 21 已将虚拟线程作为正式能力提供。下面的最小服务骨架把每个请求交给一个虚拟线程,并通过 try 作用域在关闭执行器时等待已提交任务结束。代码中的 callRemoteService 可以替换成 JDBC、HTTP Client 或文件 API;示例重点是线程模型,而不是某个框架的路由写法。
import java.time.Duration;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
static String handleRequest(String request) throws Exception {
// 每个任务创建一个虚拟线程,避免把虚拟线程当作需要复用的昂贵资源。
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
var remote = executor.submit(() -> callRemoteService(request));
// get() 只会让当前虚拟线程等待,不应把等待误判为 CPU 计算。
return remote.get();
} catch (InterruptedException e) {
// 保留中断标记,让上层能够停止取消后的请求处理。
Thread.currentThread().interrupt();
throw e;
} catch (ExecutionException e) {
// 对外抛出业务异常时保留原始 cause,便于记录真正的 I/O 错误。
throw new IllegalStateException("下游调用失败", e.getCause());
}
}
static String callRemoteService(String request) throws InterruptedException {
// 这里代表一个可能阻塞的网络或数据库调用;生产代码应设置超时。
Thread.sleep(Duration.ofMillis(20));
return "processed:" + request;
}
不要在每个请求里创建一个长期存活的共享执行器。更常见的做法是把执行器作为应用组件创建并复用,任务提交仍然是“一任务一虚拟线程”。如果业务希望并发地调用多个下游服务,可在同一个作用域提交多个任务,再集中等待和处理异常。
给数据库和下游接口加独立并发闸门
虚拟线程便宜,不代表数据库连接、第三方配额或文件句柄无限。把固定线程池改成虚拟线程后,最容易出现的回归是请求进来了很多,但所有请求同时争抢一个只有几十个连接的资源池。此时不应重新把虚拟线程池改小,而应在资源边界使用 Semaphore 或让连接池本身承担排队。
import java.util.concurrent.Semaphore;
final class DownstreamGate {
// permits 表示允许同时占用的下游名额,而不是虚拟线程总数。
private final Semaphore permits = new Semaphore(32);
String query(String key) throws Exception {
permits.acquire();
try {
// 只有真正占用下游连接的区间才持有许可,减少无谓排队。
return blockingQuery(key);
} finally {
// 无论查询成功、失败还是取消,都必须归还名额。
permits.release();
}
}
}

CPU 密集阶段使用有界平台线程执行器
如果请求先读取数据,再做大规模排序或压缩,可以让虚拟线程负责前后的 I/O,把计算段提交到大小接近处理器数量的有界执行器。这样做的目的不是追求一个固定数字,而是避免无界请求把大量 CPU 任务同时排入内存;具体大小应根据压测、任务时长和业务延迟目标调整。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
final class Workloads implements AutoCloseable {
private final ExecutorService cpuExecutor =
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
String process(String key) throws Exception {
// 外层可运行在虚拟线程,阻塞读取完成后才进入 CPU 阶段。
var input = blockingRead(key);
var future = cpuExecutor.submit(() -> expensiveTransform(input));
// 等待计算结果时保留中断语义,不吞掉取消信号。
return future.get();
}
@Override
public void close() {
// 应用退出时关闭平台线程执行器,避免遗留非守护线程。
cpuExecutor.shutdown();
}
}
迁移后检查锁、资源和可观测性边界
迁移并不是把线程工厂名替换掉就结束。长时间阻塞的 synchronized 区域可能使虚拟线程 pin 住承载平台线程;应缩小锁保护范围,并用 JFR 的虚拟线程事件观察实际影响。线程本地变量也要重新评估:虚拟线程不复用,不能再依靠“同一个线程反复拿到同一个昂贵连接”来做资源缓存。
| 工作负载 | 建议模型 | 边界检查 |
|---|---|---|
| 大量 HTTP、JDBC 或文件等待 | 按任务创建虚拟线程 | 超时、取消、连接池和下游配额 |
| 排序、压缩、加密等计算 | 有界平台线程执行器 | 处理器利用率、队列和尾延迟 |
| 同一共享状态的复合更新 | 缩小锁范围或改为消息串行化 | 避免在锁内执行阻塞 I/O |
| 迁移后的异常排查 | JFR、日志关联和压测 | pinning、内存、取消和资源泄漏 |
常见问题
虚拟线程是否应该放进固定大小线程池?
通常不应该。虚拟线程应按任务创建;如果要限制数据库或第三方服务并发,应限制那个具体资源,而不是用线程池伪装资源闸门。
阻塞 I/O 一定会释放平台线程吗?
JDK 支持的阻塞 I/O 通常可以挂起虚拟线程,但长时间持有某些锁、调用不配合的本地代码或外部库时,仍要通过 JFR 和压测确认是否出现 pinning。
虚拟线程会降低单次 CPU 任务耗时吗?
不会。它主要改善高并发等待型任务的吞吐;CPU 密集任务仍受处理器核心数限制,应该使用有界的计算执行器。
落地时可以先选择一条以 I/O 等待为主的接口,记录连接池利用率、P95/P99 延迟、CPU 和取消率,再用虚拟线程替换原来的请求执行模型。只要把请求并发、外部资源并发和 CPU 并发拆成三个可以独立观测的数字,线程模型就不容易在扩容时失控。
-
378 收藏
-
365 收藏
-
346 收藏
-
118 收藏
-
383 收藏
-
306 收藏
-
文章 · java教程 | 11小时前 | Java教程 · CompletableFuture · Java HttpClient Java异步请求超时 CompletableFuture异常处理 HttpTimeoutException sendAsync异常读取278 收藏
-
448 收藏
-
345 收藏
-
文章 · java教程 | 4天前 | 虚拟线程 · Java StructuredTaskScope Java结构化并发 Java子任务取消 StructuredTaskScope结果汇总 Java虚拟线程并发117 收藏
-
149 收藏
-
305 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习