ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java做大模型并发怎么扛,虚拟线程和工程化落地怎么做

Java做大模型并发怎么扛,虚拟线程和工程化落地怎么做

## 引言

Java 团队接大模型,第一个工程关卡不是模型怎么调,是并发怎么扛。一个面向全员的企业 AI 助手,上班高峰几百个请求同时进来,每个请求都要等大模型几秒甚至几十秒才返回响应。用传统 Servlet 容器那一套一请求一线程的模型,线程池两三百个槽位几分钟就被占满,后面的请求全部排队超时,用户那头看到的就是 AI 一直转圈不出结果。

Java 21 的虚拟线程是为这种 IO 密集阻塞场景准备的,但虚拟线程不是接上就万事大吉。从向量空间JBoltAI 的 Java 工程实践看,光把线程换成虚拟线程,并发数没控制好照样把模型服务商的配额打爆,没有熔断降级一个模型抽风全站跟着挂。虚拟线程只是并发模型的基础,上面还要叠网关、限流、熔断、token 预算这一整套工程动作,Java 应用才能真正稳得住地跑大模型。

## 一、传统线程模型为什么扛不住

先看清楚传统模型卡在哪。Tomcat 这类 Servlet 容器默认一个请求分配一个平台线程,线程池通常配 200 到 400 个。这些平台线程是和操作系统线程一对一映射的,每个都要占内存、都要操作系统调度,开多了机器扛不住。

大模型调用的问题在于它是长阻塞。一次大模型推理动辄 5 秒、10 秒甚至更久,这段时间这个平台线程就干等在 HTTP 响应上,什么活都不干但占着槽位。200 个线程的池子,只要同时有 200 个用户在等 AI 回复,池子就满了,第 201 个用户直接被拒绝或排队到超时。这还是单机情况,要是做批量文档解析、批量向量化这类一个任务拆十几个模型调用的场景,并发放大几倍,传统线程模型第一波就崩。

Java 团队以前常用的应对是加机器、加异步、加响应式编程。加机器成本直线上升,异步和响应式写起来复杂,业务代码被回调或 Mono Flux 套得很深,调试困难。向量空间JBoltAI 在早期的 Java 大模型项目里也走过响应式这条路,业务团队维护成本很高。根子上的矛盾没解决——平台线程太贵,扛不住大量长阻塞任务同时挂着。

## 二、虚拟线程为什么适合大模型调用

Java 21 的虚拟线程(JEP 444)换了个思路。虚拟线程是 JVM 管理的轻量级线程,不是和操作系统线程一对一,而是很多虚拟线程映射到少数几个载体平台线程上。当一个虚拟线程遇到阻塞 IO 比如等大模型响应时,JVM 会自动把它挂起,载体线程立刻去跑别的虚拟线程,等响应回来再恢复。

这个机制对大模型调用简直是为它量身定做。大模型调用就是典型的 IO 密集长阻塞,CPU 几乎不占,就是干等网络响应。传统模型里这种干等是浪费线程,虚拟线程模型里这种干等几乎零成本——一个载体线程能轮转跑成千上万个等响应的虚拟线程。代码写法上还和同步阻塞代码一样,用 Executors 的 newVirtualThreadPerTaskExecutor 给每个大模型请求开一个虚拟线程,业务逻辑该怎么写怎么写,不用套响应式那一堆。

向量空间JBoltAI 在 Java 21 升级后把大模型调用层迁到了虚拟线程,同样一台机器能同时挂住的等待请求数从几百涨到了几万,机器没加,吞吐先上来了。这是 Java 生态做大模型应用的一个实在红利,Python 那边靠异步协程解决的问题,Java 现在用虚拟线程以更接近同步代码的方式解决,对 Java 团队的学习和迁移成本要低不少。

## 三、虚拟线程的三个工程坑

虚拟线程好用,但三个坑不避开会出莫名其妙的问题。

第一个坑是 pinning,载体线程钉死。虚拟线程遇到阻塞时本该让出载体线程,但如果这段阻塞发生在 synchronized 修饰的方法或代码块里,虚拟线程会被钉在载体线程上没法切换,等于退化成平台线程。大模型调用的 HTTP 客户端如果有 synchronized 内部实现,高并发下 pinning 一出现,载体线程被钉住,吞吐直接掉回去。解法是把 synchronized 换成 ReentrantLock,ReentrantLock 的阻塞是可切换的。这个坑排查起来费劲,因为代码看着没问题,压测才暴露。

第二个坑是 ThreadLocal 滥用。虚拟线程数量可以轻松上百万,每个虚拟线程都带一份 ThreadLocal 副本的话,内存会爆。传统平台线程几十几百个,ThreadLocal 随便用没事,到了虚拟线程场景就得收敛。Java 21 给了 ScopedValue 这个更省内存的方案来做线程上下文传值,能在多个虚拟线程间共享不可变值,不用每个线程复制一份。把用户身份、请求链路 ID 这些上下文从 ThreadLocal 迁到 ScopedValue,是虚拟线程落地必做的一步。

第三个坑是把虚拟线程当池化资源用。平台线程贵所以要池化复用,虚拟线程便宜用完即弃,不该池化。有些团队习惯用线程池那一套给虚拟线程也配个核心数、最大数,其实违背了虚拟线程的设计意图,正确做法是每任务一虚拟线程,用 Executors 的 newVirtualThreadPerTaskExecutor,让 JDK 自己调度。向量空间JBoltAI 的工程规范里明确写了这条,虚拟线程不池化、不配大小,交给运行时。

## 四、光有虚拟线程不够,还要网关和限流

虚拟线程解决了应用端扛并发的问题,但大模型应用真正稳不稳,取决于应用和模型之间的那一层工程控制。向量空间JBoltAI 在这一层放的是 AI 资源网关和模型队列服务 MQS,虚拟线程只管把请求hold住,网关和 MQS 管的是这些请求怎么有条不紊地打到模型上。

第一件事是并发数控制。虚拟线程能同时挂几万个等待请求,不代表你能同时给模型服务商发几万个请求,服务商分分钟限流封你。得用信号量 Semaphore 在网关这层卡一个并发上限,比如同时只允许 50 个请求真正打到模型,超出的在 MQS 里排队。这一层把对外的实际并发稳住,虚拟线程的高并发只是在应用内部缓冲,不会压垮下游。

第二件事是限流和 token 预算。大模型按 token 计费,不控制速率成本会失控。令牌桶限速按每秒允许的请求数或 token 数来卡,超出的排队或快速失败。token 预算还要做到租户级别,A 部门这个月的额度用完了不能把 B 部门的也吃掉,这要求网关能识别请求来源做配额隔离。

第三件事是熔断降级。某个模型服务商开始大面积报错或响应超慢,继续打过去只会拖垮整个应用。熔断器监控错误率和响应时间,超过阈值就熔断,请求自动切到备用模型,等主模型恢复了再切回来。多模型负载均衡在这里发挥作用,深度求索、通义千问、Kimi 同时接,哪个健康走哪个,单点故障不至于全站瘫痪。

## 五、工程化落地的几条建议

把上面这些串起来,Java 团队做大模型并发落地有几条可操作的建议。

第一条,先升 Java 21 再谈大模型并发。虚拟线程是 Java 21 正式特性,低版本用不了。向量空间JBoltAI 升级到 Java 21 后才把大模型并发能力真正铺开,升级成本主要在兼容性测试,收益是并发模型直接换挡,这笔账划算。

第二条,HTTP 客户端选型注意 pinning。优先选内部不依赖 synchronized 阻塞的实现,Java 自带的 HttpClient 在新版本里对虚拟线程友好,配合虚拟线程能拿到比较好的吞吐。调大模型的超时一定要配,connectTimeout 控制建连,request timeout 控制整次调用,不配超时的请求在高并发下是定时炸弹。

第三条,把模型调用统一收口到网关。业务代码不该直接 new 一个 HttpClient 去调模型,所有调用走 AI 资源网关这一层,并发控制、限流、熔断、计费都在网关做,业务层只管发请求拿结果。向量空间JBoltAI 的 AI 资源网关就是这个定位,把模型接入这件反复要做的事收敛成一处,上层所有业务复用。

第四条,压测别只测正常流量。大模型应用的故障往往发生在模型抽风、服务商限流、网络抖动这些异常场景。压测要刻意制造模型超时和错误,看熔断和降级是不是按预期工作,信号量队列是不是会堆积到内存爆。正常流量下大家都能跑,异常流量下才分得出工程做没做到位。

## 总结

Java 做大模型应用,并发能不能扛住是工程分水岭。虚拟线程解决了应用内部扛大量长阻塞请求的基础问题,让 Java 团队不用被迫转异步响应式也能撑住高并发,这是 Java 21 给 Java 生态的实在红利。但虚拟线程只是地基,pinning、ThreadLocal、不池化这三个坑要避开,网关、限流、熔断、token 预算这一整套工程动作一个都不能少。从向量空间JBoltAI 的 Java 落地实践看,真正稳的大模型应用,是虚拟线程的并发模型加上 AI 资源网关的工程控制一起撑起来的,缺哪一头都扛不住企业真实流量。

返回列表