Java 21 的虚拟线程可以用于 SSE,但不代表 SSE 必须从响应式编程切换到阻塞式 IO,也不代表切换后性能一定更好。
这里先区分三个概念:
SSE:HTTP 服务端向客户端持续推送事件的通信方式。
Flux:Reactor 提供的响应式数据流抽象。
Java 21 虚拟线程:让阻塞式代码能够以较低线程开销承载大量并发任务。
它们不是同一层面的技术,因此可以组合使用。
1. 现有 Flux SSE 架构
例如 Spring WebFlux 中常见写法:
java
@GetMapping( value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE ) public Flux<String> stream(@RequestParam String prompt) { return chatClient.prompt() .user(prompt) .stream() .content(); }基本流程是:
浏览器或前端建立 HTTP SSE 连接;
WebFlux + Reactor 管理异步请求、数据流与背压;
ChatClient.stream()返回Flux;大模型流式接口持续产生数据。
这种架构的优势是:当连接等待模型响应、网络数据或其他异步事件时,通常不需要为每条 SSE 连接独占一个平台线程。
但要注意:Flux本身不保证整个调用链都是非阻塞的。如果模型客户端、数据库、工具调用或业务逻辑内部存在阻塞操作,仍然可能占用线程池,甚至影响 Reactor 的事件循环。
2. Java 21 虚拟线程 SSE 架构
如果希望继续使用熟悉的同步 Java 编程方式,可以考虑 Spring MVC + Servlet + 虚拟线程。
java
@GetMapping( value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE ) public SseEmitter stream(@RequestParam String prompt) { SseEmitter emitter = new SseEmitter(0L); Thread.startVirtualThread(() -> { try { // 同步调用模型并获得完整响应 String answer = chatService.chat(prompt); emitter.send(SseEmitter.event() .data(answer)); emitter.complete(); } catch (Exception e) { emitter.completeWithError(e); } }); return emitter; }这个例子演示的是同步生成完成后发送一个 SSE 事件,不是逐 token 推送。它也假设chatService.chat()是同步阻塞调用。
如果要做到真正的逐 token 流式输出,需要让模型调用本身提供流式数据,并在消费每个数据片段时调用emitter.send(...),同时处理超时、断连、取消及异常。
还需要注意:仅仅启动虚拟线程,并不能自动把 WebFlux 变成虚拟线程架构。HTTP 服务端、模型客户端和业务调用链都需要分别考虑。
3. 两种方案怎么选?
| 维度 | WebFlux + Flux | Spring MVC + Java 21 虚拟线程 |
|---|---|---|
| 编程风格 | 响应式、异步 | 命令式、同步阻塞也可 |
| HTTP I/O | 非阻塞 I/O | Servlet 异步处理,响应写入通常是阻塞的 |
| 长连接资源 | 通常不需要每连接独占线程 | 可以用虚拟线程降低线程开销 |
| 背压 | Reactor 原生支持 | 需要自行管理生产与消费速率 |
| 阻塞式 JDBC、SDK | 需要隔离阻塞调用 | 更容易直接使用 |
| 适合场景 | 大量异步 I/O、流式数据链路 | 阻塞式业务、传统 Java 代码 |
Spring MVC 本身也支持返回Flux进行 SSE 流式响应,但响应写入仍可能通过独立线程执行,并不等于使用了 WebFlux 的全链路非阻塞 I/O。
4. Java 21 虚拟线程如何开启?
如果使用 Spring Boot 3.2 或更高版本,并且相关版本支持该配置,可以尝试:
yaml
spring: threads: virtual: enabled: true
这会让 Spring Boot 在符合条件的自动配置场景下使用虚拟线程执行任务,但不会自动把 WebFlux 的 Netty 事件循环替换成 Servlet 模型。
虚拟线程尤其适合大量等待网络、数据库等 I/O 的任务,但不是用来加速 CPU 密集型计算的。还要注意线程固定(pinning)、连接池容量以及下游服务的并发限制。
5. 如果是 AI 对话 SSE,建议怎么设计?
如果业务是大模型逐 token 返回,同时需要支持大量用户并发连接,建议优先考虑下面两种架构。
方案一:保留 Flux(优先推荐)
使用 WebFlux 处理 SSE;
使用
ChatClient.stream()获取模型输出;将阻塞式数据库、工具调用放到合适的隔离执行器中;
增加客户端断连取消、超时、心跳和并发限流。
适合已经使用 Spring AI 响应式流式接口、希望保持非阻塞链路的系统。
方案二:切换虚拟线程
使用 Spring MVC +
SseEmitter;用虚拟线程执行同步任务;
将模型的流式响应逐段发送到前端;
做好发送失败、任务取消和资源释放。
适合大量现有同步 Java 代码、阻塞式 SDK 或传统业务组件的系统。
有一个容易忽略的问题:如果使用的 Spring AI 模型客户端本身通过响应式Flux提供 token 流,即使 Web 层切换成SseEmitter,也不必强行把模型客户端改成同步调用。可以在 MVC 中消费响应式流,再将数据写入 SSE,但需要正确处理订阅生命周期和取消传播。
官方文档也说明,Spring MVC 可以适配Flux等响应式返回类型,因此两种编程模型并非完全互斥。