在高并发时代,一个 HTTP 请求的处理方式,直接决定了系统的吞吐量与资源利用率。 从最早的 同步阻塞,到 异步阻塞,再到 真正的非阻塞,Java Web 技术栈经历了深刻的演进。
本文将通过典型代码示例,带你看清:
- 每个阶段 “阻塞”的到底是什么?
- 线程、I/O、容器 三者如何互动?
- 为什么 WebFlux 在 API 网关等场景中成为首选?
一、背景:什么是“阻塞”?谁被阻塞了?
在 Web 服务器中,一次 HTTP 请求处理涉及两个关键资源:
| 资源 | 说明 |
|---|---|
| 线程(Thread) | Tomcat/Netty 分配的工作线程,用于执行你的业务逻辑 |
| I/O(Input/Output) | 读写网络数据(如接收请求体、调用数据库、调用远程 API) |
🔥 “阻塞” = 线程在等待 I/O 完成时,无法做其他事。 此时线程被“挂起”,但 CPU 和内存仍被占用 —— 这是性能瓶颈的根源。
二、阶段 1:Servlet 2.x —— 同步阻塞(The Classic Model)
✅ 典型代码(Spring Boot Controller)
@GetMapping("/sync")
public String handleSync() throws InterruptedException {
// 模拟耗时操作:调用外部服务(阻塞 I/O)
Thread.sleep(1000); // 代表 JDBC / RestTemplate / File I/O
return "Done";
}
❌ 阻塞点分析:
- 线程阻塞:Tomcat 工作线程被
sleep()(或InputStream.read())完全占用; - I/O 阻塞:即使没有 sleep,
request.getInputStream().read()也会阻塞直到数据到达; - 资源浪费:1000 并发 → 1000 线程 → 内存爆炸 + 上下文切换开销大。
📌 本质:1 请求 = 1 线程 = 1 阻塞 I/O 这是传统 Java Web 应用的默认模式。
三、阶段 2:Servlet 3.0 —— 异步但仍是阻塞 I/O
💡 目标:释放 Tomcat 主线程,但 I/O 本身仍阻塞。
✅ 典型代码(使用 Spring DeferredResult)
@GetMapping("/async-blocking")
public DeferredResult<String> handleAsyncBlocking() {
DeferredResult<String> result = new DeferredResult<>();
executorService.submit(() -> {
try {
Thread.sleep(1000); // ⚠️ 仍然是阻塞 I/O!
result.setResult("Done");
} catch (Exception e) {
result.setErrorResult(e);
}
});
return result; // 主线程立即返回
}
🔧 裸 Servlet 实现:看清 AsyncContext 的本质
Spring 的 DeferredResult 是对底层 AsyncContext 的封装。让我们用原生 Servlet 看看它是如何工作的:
@WebServlet(urlPatterns = "/async-native", asyncSupported = true)
public class AsyncNativeServlet extends HttpServlet {
private final ExecutorService executor = Executors.newFixedThreadPool(10);
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
// 1. 启动异步上下文
AsyncContext asyncContext = req.startAsync();
asyncContext.setTimeout(30_000);
// 2. 提交耗时任务到业务线程池
executor.submit(() -> {
try {
// ⚠️ 模拟阻塞 I/O(如调用数据库或远程服务)
Thread.sleep(2000);
// 3. 通过原请求/响应对象写回结果
asyncContext.getResponse().getWriter().write("Hello after 2s");
// 4. 标记异步处理完成
asyncContext.complete();
} catch (Exception e) {
e.printStackTrace();
asyncContext.complete();
}
});
// ← 主线程执行到这里就返回了!Tomcat 线程已释放
}
}
🧠 关键机制解析:
asyncSupported = true:必须声明,否则startAsync()抛异常;req.startAsync():容器挂起连接,不自动提交响应;- 主线程立即返回,Tomcat 工作线程被释放;
- 业务线程池中的线程仍然阻塞(
sleep()或RestTemplate); - 调用
asyncContext.complete()后,容器才真正发送响应并关闭连接。
💡 与 Spring 的关系:
DeferredResult内部正是封装了AsyncContext+ 回调注册,让异步逻辑更声明式。
💡 深入解析:主线程 return 后,客户端在等什么?
很多人疑惑:主线程执行完就返回了,客户端是不是已经收到响应了?
答案是否定的!客户端仍在等待,TCP 连接保持打开。
- 容器通过
AsyncContext将连接挂起(suspended); - 客户端(如浏览器)处于 “loading”状态;
- 直到
complete()被调用,才通过原 TCP 连接写回响应。
✅ 关键理解: “方法返回” ≠ “HTTP 响应发送”。 在异步模型中,
return只是告诉容器:“我稍后会完成响应”。
四、阶段 3:Servlet 3.1 —— 真正的非阻塞 I/O(仅限请求/响应体)
💡 目标:连业务线程也不需要,通过事件回调处理 I/O。
✅ 典型代码(裸 Servlet,处理流式上传)
@WebServlet(urlPatterns = "/nio", asyncSupported = true)
public class NioServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
AsyncContext async = req.startAsync();
ServletInputStream input = req.getInputStream();
input.setReadListener(new ReadListener() {
private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
@Override
public void onDataAvailable() throws IOException {
byte[] b = new byte[1024];
while (input.isReady()) { // 关键:避免阻塞
int len = input.read(b);
if (len > 0) buffer.write(b, 0, len);
}
}
@Override
public void onAllDataRead() throws IOException {
System.out.println("Received: " + buffer.size() + " bytes");
async.getResponse().getWriter().write("OK");
async.complete();
}
@Override
public void onError(Throwable t) {
async.complete();
}
});
}
}
✅ 非阻塞点分析:
- 无任何线程被 socket I/O 阻塞;
- 容器通过 I/O 多路复用(如 epoll)监听多个连接;
- 数据就绪时,才回调
onDataAvailable()。
💡 深入解析:如果 onDataAvailable 调用外部 HTTP,还非阻塞吗?
这是一个致命陷阱!
@Override
public void onDataAvailable() {
// ... 读取请求体
String resp = restTemplate.getForObject("https://api.example.com", String.class); // ❌
}
后果:
onDataAvailable()在 Tomcat 的 I/O 线程(EventLoop)上执行;RestTemplate会阻塞该线程,导致所有其他连接无法处理;- 整个非阻塞模型崩溃。
⚠️ Servlet 3.1 的非阻塞仅限于“socket 读写”, 它无法保证业务逻辑中的调用也是非阻塞的。
那客户端连接还在吗?
是的!
只要不调用 async.complete(),TCP 连接就保持打开(受超时限制)。
但因 I/O 线程被阻塞,响应会严重延迟甚至失败。
💡 深入解析:异步非阻塞是否仍占用 TCP 连接?
是的!而且必须占用。
- 无论是哪种模型,只要 HTTP 响应未完成,TCP 连接就必须保持打开;
- 否则客户端会收到
Connection reset,协议无法工作。
🔑 但关键区别在于线程使用方式:
- 同步:1 连接 = 1 线程(强绑定);
- 异步非阻塞:N 连接 = 1~few 线程(通过 epoll/kqueue 多路复用)。
✅ 异步非阻塞节省的不是连接,而是线程! 它让单机支撑 10万+ 并发连接 成为可能。
五、阶段 4:Spring WebFlux —— 全链路响应式非阻塞(API 网关实战)
💡 目标:构建高并发 API 网关,聚合多个下游服务,全程非阻塞
在微服务架构中,API 网关 是 WebFlux 的黄金场景:
- 面向成千上万客户端连接;
- 需并行调用多个下游服务(用户、订单、推荐等);
- 对延迟和资源利用率极度敏感。
我们通过一个 “获取用户主页” 接口来演示。
🧩 架构示意
[Client]
│
└─ WebClient ──→ [API Gateway (WebFlux)]
├─→ UserService (HTTP)
├─→ OrderService (HTTP)
└─→ RecommendService (HTTP)
✅ 正确实现:全链路非阻塞网关
1. 客户端(调用方)
// 模拟前端或移动端
public class ClientApp {
private final WebClient gatewayClient = WebClient.builder()
.baseUrl("http://localhost:8080")
.build();
public void fetchProfile() {
Mono<ProfileResponse> profile = gatewayClient
.get().uri("/profile/123")
.retrieve()
.bodyToMono(ProfileResponse.class);
profile.subscribe(response -> {
System.out.println("Received: " + response.getUsername());
});
}
}
2. API 网关(WebFlux 服务端)
@RestController
public class ProfileGateway {
private final WebClient userClient = WebClient.create("http://user-service");
private final WebClient orderClient = WebClient.create("http://order-service");
private final WebClient recommendClient = WebClient.create("http://recommend-service");
@GetMapping("/profile/{userId}")
public Mono<ProfileResponse> getProfile(@PathVariable String userId) {
// 并行发起三个非阻塞调用
Mono<User> userMono = userClient.get()
.uri("/users/" + userId)
.timeout(Duration.ofSeconds(2))
.retrieve().bodyToMono(User.class)
.onErrorReturn(new User("unknown"));
Mono<List<Order>> ordersMono = orderClient.get()
.uri("/orders?userId=" + userId)
.timeout(Duration.ofSeconds(2))
.retrieve().bodyToFlux(Order.class).collectList();
Mono<List<RecommendItem>> recommendsMono = recommendClient.get()
.uri("/recommend?(userId=" + userId)
.timeout(Duration.ofSeconds(2))
.retrieve().bodyToFlux(RecommendItem.class).collectList();
// 合并结果(zip 在所有完成时触发)
return Mono.zip(userMono, ordersMono, recommendsMono)
.map(tuple -> new ProfileResponse(
tuple.getT1(), // User
tuple.getT2(), // Orders
tuple.getT3() // Recommendations
));
}
}
🧠 关键优势:
- 三个 HTTP 调用并行发起(非串行);
- 无任何线程阻塞:Netty EventLoop 注册回调,I/O 就绪时触发;
- 单个 EventLoop 线程可同时处理数千个此类请求;
- 总耗时 ≈ max(各服务耗时),而非 sum。
✅ 这才是 WebFlux 的核心价值:用极少线程,高效协调多个 I/O 操作。
❌ 错误实现:看似响应式,实则退化为阻塞
@GetMapping("/profile/bad")
public Mono<ProfileResponse> getBadProfile(@PathVariable String userId) {
return Mono.fromSupplier(() -> {
// ❌ 全是阻塞调用!
User user = restTemplate.getForObject("http://user-service/users/" + userId, User.class);
List<Order> orders = restTemplate.getForObject("http://order-service/orders?userId=" + userId, Order[].class);
List<RecommendItem> recs = restTemplate.getForObject("http://recommend-service/recommend?userId=" + userId, RecommendItem[].class);
return new ProfileResponse(user, orders, recs);
}).subscribeOn(Schedulers.boundedElastic()); // “打补丁”
}
⚠️ 问题分析:
- 虽然返回
Mono,但内部全是 阻塞 I/O; subscribeOn(boundedElastic())只是把阻塞任务扔到线程池;- 每个请求仍占用一个线程 200ms+;
- 10,000 并发 → 10,000 线程 → 内存爆炸;
- 比直接用 Spring MVC 更差(多了响应式调度开销)。
📉 后果:你部署了 WebFlux,却得到了 Tomcat + 大量线程池的混合体,资源消耗更高,吞吐更低。
🔥 开发中三大致命隐患(网关场景特有)
| 隐患 | 表现 | 后果 |
|---|---|---|
| 1. 串行调用下游 | 先查用户 → 再查订单 → 再查推荐 | 总延迟 = T1 + T2 + T3,浪费 I/O 等待时间 |
| 2. 混用阻塞客户端 | 在 Mono 中调用 RestTemplate | EventLoop 被阻塞(若未切线程)或线程池爆炸(若切了) |
| 3. 忘记超时控制 | webClient.get().retrieve().bodyToMono() 无超时 | 下游慢 → 网关连接堆积 → OOM |
✅ 正确做法:
- 使用
timeout()设置合理超时; - 使用
onErrorReturn/onErrorResume降级; - 永远不要调用
.block()。
🛠 如何验证网关是否真正非阻塞?
-
线程数监控
jstack <pid> | grep "reactor-http-nio" | wc -l- 正常值:4~8(≈ CPU 核数);
- 异常值:数百 → 说明用了
boundedElastic或阻塞调用。
-
压测对比
- 工具:wrk、JMeter;
- 指标:吞吐量 / 线程数;
- WebFlux 应在 低线程下达到高 QPS。
-
APM 链路追踪
- 使用 SkyWalking 或 Zipkin;
- 正确链路:单个 trace 包含多个并行 span;
- 错误链路:串行 span + 大量线程切换。
🔁 为什么网关特别适合 WebFlux?
| 特性 | 传统 Servlet 网关 | WebFlux 网关 |
|---|---|---|
| 并发连接数 | 受限于线程池(通常 < 1000) | 可达 10万+ |
| 资源消耗 | 高(线程栈内存) | 极低 |
| 下游调用 | 串行 or 线程池并行 | 原生并行(无额外线程) |
| 延迟 | 高(线程调度开销) | 低(事件驱动) |
💡 结论: 如果你的服务是 I/O 密集型 + 高并发 + 聚合多个下游, WebFlux 不是“可选项”,而是“最优解”。
六、核心对比表:阻塞点在哪?
| 模型 | Tomcat 线程是否阻塞? | 业务线程是否阻塞? | I/O 是否阻塞? | 能否突破线程数限制? | TCP 连接是否保持? |
|---|---|---|---|---|---|
| Servlet 2.x | ✅ 是 | ❌(无) | ✅ 是 | ❌ 否 | ✅ 是 |
| Servlet 3.0 | ❌ 否 | ✅ 是 | ✅ 是 | ❌ 否(依赖线程池) | ✅ 是 |
| Servlet 3.1 | ❌ 否 | ❌(无) | ❌ 否(仅限请求/响应体) | ✅ 是(少量线程) | ✅ 是 |
| WebFlux | ❌(不用 Tomcat) | ❌(无) | ❌ 否(全链路) | ✅ 是(EventLoop) | ✅ 是 |
📌 所有模型都保持 TCP 连接直到响应完成,区别只在线程使用方式。
七、如何选择?—— 实战建议
| 场景 | 推荐方案 |
|---|---|
| 传统 CRUD + MySQL | Servlet 2.x / 3.0 + 连接池优化(简单高效) |
| 高并发 API 网关、实时推送 | WebFlux + Netty(全链路非阻塞) |
| 大文件流式上传(MVC 项目) | Servlet 3.1 + ReadListener(特定优化) |
| 后台任务解耦 | @Async + 线程池(非 Web 层异步) |
🌟 记住:
- 不要为了“非阻塞”而强行上 WebFlux;
- 80% 的应用,优化数据库 + 缓存 + 线程池,比切换模型更有效;
- 只有当你真正遇到 I/O 密集型高并发瓶颈时,才考虑 WebFlux。
🔚 总结:演进的本质
| 阶段 | 解决的问题 | 未解决的问题 |
|---|---|---|
| Servlet 2.x | 快速开发 Web 应用 | 线程资源浪费 |
| Servlet 3.0 | 释放容器线程 | 业务线程仍阻塞 |
| Servlet 3.1 | 请求/响应体非阻塞 | 下游调用仍阻塞(且难整合) |
| WebFlux | 全链路非阻塞 + 生态支持 | 学习成本高、生态要求严 |
💡 技术演进不是替代,而是扩展。 理解每种模型的适用边界,才是架构师的核心能力。
✅ 终极认知: “连接是网络资源,线程是计算资源”。 异步非阻塞的本质,是用少量计算资源(线程)高效管理大量网络资源(连接)。