JDK 21虚拟线程实战:Spring Boot 3.5让AI推理并发量翻倍

发布时间:2026/9/29 17:44:35
JDK 21虚拟线程实战:Spring Boot 3.5让AI推理并发量翻倍 最近花了一周时间把一个核心 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/UndertowSpring 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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.5.0/version relativePath/ /parent properties java.version21/java.version maven.compiler.source21/maven.compiler.source maven.compiler.target21/maven.compiler.target /properties依赖只需要常规 web 启动器如果你用 WebClient 调用推理服务再引入 webflux 相关依赖即可dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-webflux/artifactId /dependency3.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 --scriptpost.lua http://localhost:8080/chatpost.lua 里构造 POST 请求体wrk.method POST wrk.headers[Content-Type] application/json wrk.body {userId:u001,message:hello}先跑平台线程模式配置 spring.threads.virtual.enabledfalse然后开启虚拟线程再跑。同一台 8C16G 机器下游模拟推理服务保持不变结果如下指标平台线程模式虚拟线程模式变化线程池配置Tomcat 默认 200 线程虚拟线程无手工设置-吞吐量 QPS4681052125%平均响应时间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 默认 maximumPoolSize10连接根本不够分。每个虚拟线程都在连接池上排队等待拿连接整体延迟飙升。处理方式主要有三个思路调大连接池上限比如从 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 RecorderJFR可以拿到更精确的阻塞事件。我在上线前专门跑了一段 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 的接口编排、超大流量的预测、自动扩缩容策略但我目前最大的心得就一句话用虚拟线程之前想清楚你的瓶颈是“算不过来”还是“等不到结果”只要属于后者它基本不会让你失望。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询