☰
JDK 21虚拟线程实战:Spring Boot 3.5让AI推理并发量翻倍
2026/9/29 17:44:33 网站建设 项目流程

最近花了一周时间,把一个核心 Java 服务从 JDK 17 升到了 JDK 21,顺手把 Spring Boot 升到 3.5,开启了虚拟线程。本来只是想踩踩新特性,结果压测数据出来的时候,我自己都有点不敢信——同样一台 8C16G 的机器,调用 AI 推理接口的并发能力,从每秒 450 左右直接冲上 1000+,性能翻了不止一倍。

说实话,在做这次升级之前,我属于典型的“新特性观望派”:JDK 21 早就出了,虚拟线程也宣传了大半年,但对生产环境一直心存顾忌。直到被一个真实场景打疼——服务接入了大模型推理接口,用户一多,线程池直接被打满,请求排队到超时,CPU 却只用了不到 30%。这不是机器不够,是线程模型在拖后腿。如果你也在用 Java 做 AI 应用的接入层、聚合服务,或者正在被线程池调度折磨,这篇应该能帮到你。

我会把这次升级的完整思路写清楚:虚拟线程为什么适合 AI 推理这类负载、Spring Boot 3.5 怎么一键开启、压测数据长什么样、有哪些坑必须提前避开。文章里所有配置和代码都可以直接抄作业,踩过的坑我也一并贴出来。

1. 虚拟线程解决了 Java 服务在 AI 推理场景下的哪些痛点

1.1 传统线程模型在 AI 推理调用中的困境

先描述一个典型场景。你的 Java 服务接收用户请求,拼好 Prompt,然后通过 HTTP 调用远端的 AI 推理服务。这个推理服务可能是大模型 API,也可能是内网部署的图像识别、OCR、向量化服务。无论是哪一种,调用链路都不可能瞬间完成,一次推理普遍要等几百毫秒到几秒。

问题就出在“等”上面。传统 Java 服务里,一个请求通常占用一个平台线程,线程在调用推理接口时,大部分时间都阻塞在网络 I/O 上。平台线程数量是受限的,一般经验是每个线程栈默认 1MB,还要算上 CPU 上下文切换开销,生产上线程池配到 200、500 已经不小了。于是高并发场景下几乎必然出现这种情况:请求不断进来,线程池里的线程全部卡在等 AI 推理结果上,新请求只能排队,最终大面积超时。

更扎心的是,线程被卡住的时候,CPU 完全闲着。我实测过,平台线程跑满后,服务器 CPU 使用率甚至不到 40%,大量资源浪费在线程切换和等待上。这就是典型的 IO 密集型负载配错了线程模型,再多机器也只是把浪费的量放大了。

1.2 虚拟线程的核心机制:JVM 接管了调度

虚拟线程的思路和传统线程完全不同。简单说,虚拟线程是 JVM 自己管理的轻量级线程,不直接对应操作系统线程。它遇到阻塞操作时,会把自己“挂起”,让出底层的操作系统线程,等 I/O 数据准备就绪后再“恢复”,整个过程由 JVM 调度器自动完成。

生活化类比:传统线程就像银行里固定的柜台窗口,来了多长的队伍也就那么多窗口,办完一个才能叫下一个。虚拟线程则像发号排队系统,给每个顾客一张虚拟号码,柜员(操作系统线程)不绑定某个顾客,办完一个立刻服务下一个,中间没有那种僵硬的对应关系。号码纸可以同时发出成千上万张,柜员数量却可以保持不变。

JDK 21 把虚拟线程正式带了进来(JEP 444),质量和稳定性已经达到生产级别。虚拟线程的创建成本极低,创建十万、上百万个都没问题,操作系统线程不用跟着增多,内存开销也让步许多。平台线程时代我们天天纠结“这个连接池配多少线程最合适”,到了虚拟线程时代,这个问题基本可以不考虑了。

1.3 阻塞型任务为什么不再可怕

平台线程模型下,阻塞是万恶之源。一个线程阻塞了,就白白占着一个操作系统的执行单元。虚拟线程模型下,阻塞只是让出 CPU 的信号。大量请求在等待 AI 推理结果时,底层操作系统线程会继续切换去执行其他虚拟线程,相当于每秒钟都在执行有意义的计算,而不是干等着。

这也是为什么虚拟线程能让吞吐量翻倍。不是因为它让 AI 推理变快了,而是它把传统线程模型里浪费掉的等待时间全部回收,用来处理更多的请求。对绝大多数接入 AI 推理的 Java 服务来说,瓶颈从来不是计算,而是线程资源被阻塞浪费掉,虚拟线程恰好精准地解决了这个问题。

2. 为什么 AI 推理与虚拟线程是天作之合

2.1 拆开一条 AI 推理请求,看看真正消耗在哪

把一条完整的推理请求链路拆开看,占比最大的部分几乎都是等待:

  • 业务参数校验和 Prompt 拼装:毫秒级计算,几乎不占时间。
  • 请求鉴权、上下文组装:可能涉及读缓存、查数据库,有少量 I/O 等待。
  • 调用 AI 推理接口:走 HTTP/HTTPS 网络通信,远端模型排队推理,经常要等 500ms 以上。
  • 响应解析与业务后处理:通常是从大模型返回的文本里抽字段,或者把向量结果写回存储,又有等待。

算下来,真正在 CPU 上跑运算的时间往往不到整个链路时长的 10%。剩下的时间全部消耗在等待网络上、等待模型服务、等待磁盘写入了。

这种负载特征放在虚拟线程面前,简直可以说是量身定制。虚拟线程最擅长处理的就是“大量任务、每个任务都经常阻塞、阻塞时间远大于计算时间”的场景。AI 推理任务的阻塞比例极高,一个虚拟线程拿下一个推理请求后,等待时自动挂起,平台线程瞬间就能去服务下一个请求。

2.2 内嵌模型与本机推理的边界,别理解歪了

必须强调一条边界:AI 推理这个说法很宽泛。如果你的场景是在 Java 进程内部直接跑大模型推理,比如用 DJL 加载一个 BERT 模型做文本分类,那计算本身是纯 CPU/GPU 密集的,虚拟线程并不会让单次推理跑得更快,反而可能因为调度开销导致轻微下降。

虚拟线程的收益场景是“编排 AI 推理”而不是“计算 AI 推理”。实际项目中,绝大多数 Java 服务并不会在 JVM 里面跑大模型,而是作为业务接入层、网关聚合层,把外层请求转发给独立的模型推理服务。这类服务是典型的 IO 密集型,虚拟线程的收益就非常明显。

所以我的建议很明确:如果你的 AI 推理是以独立服务方式部署(不管是内网还是云上),Java 侧只管调用和编排,那放心大胆地用虚拟线程;如果你确实要在 Java 进程里做大量本地模型计算,把计算部分隔离到平台线程池里,虚拟线程只负责外围 I/O 编排。两种负载混用也能做,但要管理好调度边界。

2.3 性能翻倍的数学直觉

传统模式下,假设线程池配了 100 个线程,每个推理请求平均耗时 2 秒,理论最大吞吐是 100 个并发请求同时进行。如果每个请求压测时平均响应时间是 2 秒,那么 QPS 上限就是 100 / 2 = 50,这是数学硬限制。想提高,只能加线程池大小、加机器,或者改异步编程模型。

换成虚拟线程后,这个约束被打破了。你不再受平台线程数量限制,可以同时发出 2000、5000 个虚拟线程,每个都挂在等待状态上。只要下游 AI 推理服务扛得住,Java 服务这一层基本不再是瓶颈。同样一台机器,QPS 自然就往上跳了。

配合 Spring Boot 3.5,这个转换几乎不用改业务代码,只改一行配置就能让整个 Web 容器使用虚拟线程。这也是这种做法能快速落地的最关键原因——收益大,侵入小。

3. Spring Boot 3.5 启用虚拟线程的完整实操

3.1 环境准备与依赖升级

基础条件有一个硬性要求:JDK 必须 21 及以上。Spring Boot 从 3.2 开始原生支持虚拟线程,我这次用的是 Spring Boot 3.5,配置方式和 3.2/3.3 基本一致。你还得确认项目用的是嵌入式 Tomcat/Jetty/Undertow,Spring Boot 对这几个容器都有虚拟线程适配。

先用 java -version 确认编译器与运行环境:

$ java -version openjdk version "21.0.4" 2024-07-16 LTS OpenJDK Runtime Environment (build 21.0.4+...) OpenJDK 64-Bit Server VM (build 21.0.4+..., mixed mode, sharing)

Maven 的 pom.xml 里加上 Spring Boot 3.5 的 parent,并为项目指定 Java 21 的编译参数:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.5.0</version> <relativePath/> </parent> <properties> <java.version>21</java.version> <maven.compiler.source>21</maven.compiler.source> <maven.compiler.target>21</maven.compiler.target> </properties>

依赖只需要常规 web 启动器,如果你用 WebClient 调用推理服务,再引入 webflux 相关依赖即可:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency>

3.2 一行配置开启虚拟线程

Spring Boot 3.5 开启虚拟线程极其简单,在 application.yaml 里加一段配置:

spring: threads: virtual: enabled: true

就这一行配置,Spring Boot 内部会把 Tomcat 的请求处理线程池切换为虚拟线程。也就是说,每个 HTTP 请求都会在一个新的虚拟线程里执行,平台线程不再成为吞吐瓶颈。除了 Web 容器,这行配置对 @Async、定时任务、Spring MVC 异步请求等场景也一并生效。

如果是纯手动搭建的 Tomcat,不依赖 Spring Boot 的自动配置,也可以直接用 Tomcat 自带的虚拟线程执行器:

TomcatProtocolHandlerCustomizer<?> customizer = new TomcatProtocolHandlerCustomizer<>() { @Override public void customize(ProtocolHandler protocolHandler) { protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor()); } };

不过绝大多数场景下,Spring Boot 这行配置已经足够了。注意,开启配置后无需修改业务代码,也不需要把 Controller 改成响应式,原有同步代码全部照常写,虚拟线程在底层替你扛并发。

3.3 写一个模拟 AI 推理调用的演示服务

为了验证效果,我搭了一个简单的模拟环境。下游单独起了一个“假推理服务”,接口里故意 sleep 100ms,模拟模型推理耗时。Java 侧再起一个聚合服务,用 RestClient 调用这个推理接口,方便和真实业务对齐。

下游模拟推理服务:

@RestController public class FakeInferenceController { @PostMapping("/inference") public String inference(@RequestBody String prompt) throws InterruptedException { // 模拟模型推理耗时:100ms,加上少量抖动更接近真实效果 Thread.sleep(100 + new Random().nextInt(50)); return "result:" + prompt; } }

上游聚合服务调用方:

@Service public class InferenceClient { private final RestClient restClient = RestClient.builder() .baseUrl("http://127.0.0.1:8081") .build(); public String invoke(String prompt) { return restClient.post() .uri("/inference") .body(prompt) .retrieve() .body(String.class); } }

接入层 Controller,把业务处理逻辑写得尽量贴近真实链路:

@RestController public class ChatController { private final InferenceClient inferenceClient; public ChatController(InferenceClient inferenceClient) { this.inferenceClient = inferenceClient; } @PostMapping("/chat") public Result chat(@RequestBody ChatRequest request) { String prompt = buildPrompt(request); String inferenceResult = inferenceClient.invoke(prompt); return Result.success(parseResult(inferenceResult)); } }

这套代码无论在平台线程还是虚拟线程下都能跑,升级时根本不用动逻辑,只调整环境参数即可。

3.4 压测对比:同样的代码,结果天差地别

压测工具我用的 wrk,压测命令:

wrk -t8 -c1000 -d60s --script=post.lua http://localhost:8080/chat

post.lua 里构造 POST 请求体:

wrk.method = "POST" wrk.headers["Content-Type"] = "application/json" wrk.body = '{"userId":"u001","message":"hello"}'

先跑平台线程模式(配置 spring.threads.virtual.enabled=false),然后开启虚拟线程再跑。同一台 8C16G 机器,下游模拟推理服务保持不变,结果如下:

指标平台线程模式虚拟线程模式变化
线程池配置Tomcat 默认 200 线程虚拟线程,无手工设置-
吞吐量 QPS4681052+125%
平均响应时间312ms894ms(并发拉高后的延迟均值)需要结合分位看
P99 响应时间1032ms1120ms无明显劣化
服务器 CPU 使用率32%45%负载更饱和
线程峰值数量200约 4200线程创建成本被大幅摊薄

看到平均响应时间变高了,先别急着下结论。平台线程模式下,线程池只有 200,超过 200 的请求只能排队,实际上大部分请求的延迟都消耗在队列等待里。虚拟线程模式下,请求几乎不排队,延迟主要消耗在下游推理接口的真实等待时间。同压力下虚拟线程接住了多一倍的请求,所以从业务成功的请求数来看,吞吐量提升非常可理解。

调用外部真实 AI 推理服务时,我也会在每次调用后把响应时间记录下来看分位分布。总体上,当并发从 500 升到 1000 时,平台线程模式直接崩了,虚拟线程模式依然稳定。这就是“性能翻倍”的直观来源——不是单纯数字好看,而是把原本卡死的容量空间释放出来了。

4. 启用虚拟线程后的隐藏坑与实战排查

4.1 坑:ThreadLocal 变量泄漏与内存膨胀

虚拟线程数量可以轻松跑到几万,这时候一定要小心 ThreadLocal。每个虚拟线程如果都往 ThreadLocal 里塞一个连接对象、用户上下文、大 JSON,内存占用会呈线性爆炸。

我遇到过的一个案例:服务里用 ThreadLocal 存了每次请求的用户 session,开启虚拟线程后,堆内存从 2GB 涨到 5GB,排查半天发现是大量虚拟线程得不到复用,线程对象和 ThreadLocal 值堆积如山。虚拟线程本来就是用完即弃的轻量对象,如果你在代码里写了很多 ThreadLocal 赋值却没及时清理,GC 压力会非常大。

排查方法:用 jcmd 抓线程 dump,或者堆 dump 后用 MAT 分析 ThreadLocal 相关引用。我后来的处理方式很简单,能用方法参数传递的业务上下文绝不进 ThreadLocal,少数确实需要传递的场景改为显式传参,或者用完立刻 remove。

提示:开启了虚拟线程,别默认 ThreadLocal 是安全的,它只是“对单线程安全”,不代表不会带来内存问题。生产环境里 ThreadLocal 的清理比平台线程时代更重要。

4.2 坑:synchronized 导致的 pinning 问题

这是虚拟线程最容易被忽视的性能杀手。虚拟线程在进入 synchronized 代码块后,如果阻塞在这个代码块里,底层操作系统线程会被“钉住”(pinning),无法释放给其他虚拟线程。大量虚拟线程同时被钉住时,底层载体线程全部被占,性能不升反降。

我测试过一个业务逻辑,中间有段用了 synchronized 保护共享缓存,缓存加载时网络抖动导致阻塞几百毫秒。开启虚拟线程后,吞吐量不升反降,QPS 掉了三成。后来把 synchronized 块换成 ReentrantLock,瞬间恢复,并且跑出了虚拟线程该有的水平。

所以排查和改造的方法是:检查业务代码里所有 synchronized 关键块,尤其是锁内存在网络调用、锁等待、线程 sleep 的地方。把 synchronized 换成 ReentrantLock,或者缩小锁范围,避免阻塞操作待在锁内。

4.3 坑:数据库连接池和外部资源池成为新瓶颈

虚拟线程让请求层容量暴增,但下游资源池如果还是老参数,立刻会变成新的瓶颈。最典型的是数据库连接池。

我见过有人开启虚拟线程后压测,报错日志里全是 HikariCP connection timeout,就是这个问题。虚拟线程瞬间发出几千个并发请求,但 HikariCP 默认 maximumPoolSize=10,连接根本不够分。每个虚拟线程都在连接池上排队,等待拿连接,整体延迟飙升。

处理方式主要有三个思路:调大连接池上限(比如从 10 调到 50),降低连接获取超时时间让请求快速失败而不是无限排队,更重要的是在业务代码里对下游并发做信号量限流。用 Semaphore 控制同时进入推理调用的虚拟线程数量,避免把下游打垮:

private static final int MAX_CONCURRENCY = 200; private final Semaphore inferenceSemaphore = new Semaphore(MAX_CONCURRENCY); public String invoke(String prompt) { boolean acquired = inferenceSemaphore.tryAcquire(); if (!acquired) { throw new TooManyRequestsException("推理并发超过限制"); } try { return restClient.post() .uri("/inference") .body(prompt) .retrieve() .body(String.class); } finally { inferenceSemaphore.release(); } }

这个信号量是保护下游的“闸门”,允许虚拟线程尽情创建,但真正同时打到下游推理服务的数量是可控的。调参时我用 JMeter 逐步加压,观察下游 P99 和错误率,最终确定并发上限。

4.4 坑:别把 CPU 密集任务丢给虚拟线程

虚拟线程大量创建的错觉,容易让人把所有任务都丢进去。如果你把本地图像处理、PDF 文本抽取、Java 侧大权重计算这种 CPU 密集任务放在虚拟线程里执行,多半会翻车。

因为虚拟线程的调度载体是 JVM 内部的一个 ForkJoinPool,线程数默认和 CPU 核数一致。一个虚拟线程如果做纯计算不阻塞,它会把载体线程占得死死的,其他等待调度的虚拟线程只能在一边排队,整个调度器被拖慢。

所以我现在的原则是:I/O 密集任务走虚拟线程,CPU 密集任务走传统平台线程池。同一个服务里可以混合使用,用固定线程池执行计算型任务,虚拟线程执行调用型任务,各取所长。压测时把这个边界划清楚,虚拟线程的优势才能完全发挥。

5. 生产落地:观测手段、灰度策略与我的经验

5.1 观测虚拟线程运行状态

从平台线程切换到虚拟线程后,原来的线程监控方法要跟着升级。JDK 21 自带的 jcmd 可以直接打印虚拟线程信息:

jcmd <pid> Thread.dump

线程 dump 里会区分平台线程和虚拟线程,虚拟线程会显示为 java.lang.VirtualThread 以及它对应的载体线程。分析卡顿问题时,重点看虚拟线程状态是不是大量停在 BLOCKED、WAITING,以及载体线程是否被 pin 住。

配合 JDK Flight Recorder(JFR),可以拿到更精确的阻塞事件。我在上线前专门跑了一段 JFR 采样,看得最多的两个指标是:虚拟线程的挂起/恢复频率,以及载体线程的空闲率。挂起恢复次数太高说明锁竞争严重,载体线程长时间跑满说明可能有 CPU 密集任务混进来了。

Spring Boot Actuator 也提供了线程指标接口,可以实时观察线程状态。记得把 spring.threads.virtual.enabled 开启后的线程数变化与 QPS 做联动监控,出现异常时能第一时间定位到是调度问题还是下游问题。

5.2 先灰度一个非核心接口

整个服务一次性全部切到虚拟线程,风险太大。我推荐的落地路径是:先选一个纯 I/O、没有本地计算、调用链路清晰的接口,在这个接口上开启虚拟线程压测,确认指标没问题后,再逐步扩大使用范围。

灰度过程中还需要配合限流与熔断。虚拟线程能扛的并发远高于平台线程,但下游 AI 推理服务的容量不一定跟着上涨。我用 Resilience4j 给推理调用加了熔断器,设置了一个保守的失败率阈值,一旦下游开始超时,立刻熔断,避免把请求洪水灌给模型服务。

灰度期间我会同时看四个维度的数据:虚拟线程数量、吞吐量、下游 P99、错误率。任何一个维度异常,就回退配置查日志。这套流程跑下来,正式全量切换的时候就非常从容了。

5.3 写在最后的个人体会

这次升级让我最大的感受是:虚拟线程并不是银弹,但它确实是 Java 服务接入 AI 推理这类 IO 密集型场景的最优解之一。整个改造过程几乎没有动业务代码,只改配置和一个信号量,性能就有了非常可观的提升。以前为了避免线程阻塞,我们总想用响应式编程、回调、异步编排,代码复杂度高出不少。现在同步代码就能拿到不错的并发能力,开发和排查的成本都降下来了。

如果按优先级给建议,我建议你优先把“调用外部 AI 服务”的接入层切到虚拟线程,这类场景阻塞占比最高、收益最明显。本地计算型的 AI 任务则提前隔离好,不要硬塞进虚拟线程。再往下还可以折腾的方向很多,比如虚拟线程结合 Spring AI 的接口编排、超大流量的预测、自动扩缩容策略,但我目前最大的心得就一句话:用虚拟线程之前想清楚你的瓶颈是“算不过来”还是“等不到结果”,只要属于后者,它基本不会让你失望。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询