Servlet 3 异步处理是什么?它能提高所有请求的性能吗?
简化版
Servlet 3 异步处理通过 request.startAsync() 释放容器的请求处理线程(Tomcat Worker 线程)——请求进入异步模式后,Worker 线程可以立即归还线程池去处理其他请求,而这个请求的耗时操作交给其他线程/回调去做,完成后再通过 AsyncContext 写响应并 complete()。它提升的是请求线程的利用率,适合等待型任务(长轮询、慢 I/O、等外部回调)。但它不能让 CPU 密集任务变快——异步只是把「等待」从 Worker 线程上挪走,不减少计算量。所以不能提高所有请求的性能。
详细版
同步 vs 异步的线程占用:
同步:Worker 线程 ──── 全程占用(含等待下游)───→ 响应完成才归还
异步:Worker 线程 ─ startAsync() → 立即归还线程池
业务线程/回调 ──── 处理耗时任务 ───→ AsyncContext.complete() 写响应
异步处理的核心代码:
@WebServlet(urlPatterns = "/async", asyncSupported = true)
public class AsyncServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
AsyncContext ctx = req.startAsync(); // 进入异步,Worker 线程可归还
ctx.setTimeout(5000); // 必须设超时
ctx.addListener(new AsyncListener() { // 处理超时/异常/完成
public void onTimeout(AsyncEvent e) { /* 超时处理 */ }
public void onError(AsyncEvent e) { /* 异常处理 */ }
public void onComplete(AsyncEvent e) { /* 完成清理 */ }
public void onStartAsync(AsyncEvent e) {}
});
bizThreadPool.submit(() -> { // 耗时任务交给业务线程池
String result = callSlowDownstream(); // 慢 I/O 等待
resp.getWriter().write(result);
ctx.complete(); // 写完必须 complete
});
}
}
关键点:
- 提升的是请求线程利用率,不是单个任务的耗时。
- 适合等待型(长轮询、慢外部 HTTP、等 MQ 结果),不适合 CPU 密集。
- 必须配套:超时、异常处理(AsyncListener)、业务线程池、
complete()。
完整版教学
一、同步请求的问题:Worker 线程被「等待」占满
传统同步 Servlet 的处理:请求进入业务代码后,Tomcat 的 Worker 线程会一直占着,直到响应完成。问题在于——如果这个请求大量时间花在等待下游 I/O(等数据库、等远程 HTTP、等消息队列),那么这个 Worker 线程就一直挂在「等待」上,什么也没干,却占着不放。
后果:Tomcat 的 Worker 线程池(maxThreads,默认 200)很快被这些「在等待的请求」占满,新请求即使很快能处理,也没有空闲 Worker 线程,只能排队——系统吞吐上不去。问题的本质是:宝贵的 Worker 线程被浪费在「干等」上。
| 请求类型 | 同步 Servlet 的线程占用 | 异步 Servlet 的价值 |
|---|---|---|
| 慢外部 HTTP,等待 800ms | Worker 陪着等 800ms | Worker 发起异步后归还,等待放到业务线程/回调 |
| 长轮询,最多等 30s | Worker 可能被挂住 30s | 连接保留,请求线程不长期占用 |
| CPU 加密计算 800ms | Worker 算 800ms | 换线程仍要算 800ms,收益很小 |
记忆钩子:Servlet 异步省的是“Tomcat Worker 陪等的时间”,不是把下游 I/O 或 CPU 计算本身变短。
二、异步处理的流程:把「等待」从 Worker 线程上挪走
Servlet 3 异步就是解决这个浪费:
- 请求进入 Servlet 后,调用
request.startAsync(),请求进入异步模式,返回一个AsyncContext。 - 此时 Worker 线程可以立即返回线程池——它不用再等这个请求处理完,可以去接别的请求。
- 这个请求的耗时操作交给其他线程(业务线程池)或异步 I/O 回调去做。
- 耗时操作完成后,通过
AsyncContext写响应,并调用complete()结束这次请求。
关键变化:Worker 线程不再全程占用,它在发起异步后就归还了,只在真正需要处理(如写响应)时才短暂占用。「等待」被转移到了别处,不再霸占 Worker 线程。
三、它提升的到底是什么(最核心的判断)
异步处理提升的是「容器请求线程(Worker)的利用率」,而不是「单个任务本身的耗时」。
- 一个数据库查询要 500ms,异步不会让它变成 100ms——查询本身该多久还是多久。
- 异步做的是:在这 500ms 的等待期间,让 Worker 线程去服务别的请求,而不是干等。这样同样数量的 Worker 线程能支撑更多的并发等待型请求——吞吐提升。
推论——如果瓶颈是 CPU(本机做重计算),异步没用甚至有害:CPU 密集任务不是在「等待」,而是实实在在占用 CPU 算。异步把它换个线程跑,CPU 计算量一点没少,反而多了线程调度和上下文传递的开销。所以异步不能提高所有请求的性能——它只对「等待型」任务有效。
假设 maxThreads = 200,单个请求耗时 1000ms,其中:
CPU 计算 50ms + 等第三方接口 950ms
同步:200 个 Worker 同时被占满,后续请求排队。
异步:Worker 只负责发起和收尾,等待 950ms 不占 Worker;
但第三方接口的 950ms 并没有消失,仍要有超时、限流和线程池控制。
四、适用场景与不适用场景
适合异步(大量时间在等待):
- 长轮询 / 服务端推送(Comet、SSE)——连接挂着等事件,不能占着 Worker 线程干等。
- 等消息队列结果、等外部回调。
- 慢的外部 HTTP 调用(调第三方接口,响应慢)。
- 文件处理回调、等待型的批处理触发。
不适合异步(没有等待,或下游已够快):
- CPU 密集计算(加密、图像处理、复杂算法)——异步只是换线程算,没意义。
- 普通短请求——本来就快,异步反而增加复杂度。
- 下游已经足够快的接口——没必要为异步而异步。
五、必须配套超时和异常处理
异步请求必须设置合理的超时并注册 AsyncListener 处理各种情况:
setTimeout():设超时时间。如果不设或设太长,下游永远不返回时,这个异步请求和相关资源会长期悬挂(连接、内存漏掉),累积起来拖垮系统。onTimeout:超时时的处理(返回错误、降级响应)。onError:异步处理出错时。onComplete:完成时的清理。
忘记设超时、忘记处理超时/异常,是异步 Servlet 最常见的线上事故——请求悬挂、资源泄漏。
六、异步不代表不需要线程池
一个常见误解:「用了异步就不占线程了」。错——异步只是不占 Tomcat 的 Worker 线程,耗时逻辑仍然要在某个线程里执行(业务线程池或异步 I/O 回调)。
所以你需要自己设计业务线程池:线程池大小、队列、拒绝策略、上下文传递(如 ThreadLocal、traceId、SecurityContext 跨线程传递)都要考虑。否则只是把 Tomcat 线程池的压力转移到业务线程池——如果业务线程池也被慢任务打满,问题依旧。
真正的收益来自:Worker 线程快速归还去处理更多请求,等待型任务在数量可控的业务线程池里排队等待,两者解耦。理想情况下配合异步 I/O(如异步 HTTP 客户端、响应式),连业务线程池的等待都省掉,才是完全非阻塞。
七、响应对象的跨线程写入与生产判断
- 异步完成前,Response 仍属于这次请求。跨线程写响应要确保只写一次、最后调
complete(),并处理客户端已断开的情况。错误路径忘记complete()是常见线上问题(请求永远不结束)。
生产落地的判断:先搞清慢在哪——
- 慢是因为下游服务响应慢(等待型)→ 异步能让 Worker 线程尽快归还,有效。
- 慢是因为本机 CPU 算法重(计算型)→ 异步只是换线程继续算,无效。
上线后要监控:异步任务队列长度、超时次数、客户端断开次数、complete() 是否成对调用——这些指标比「用了异步」本身更能说明系统健康与否。
八、常见误区与追问
- 误区:Servlet 异步能让所有接口变快。 它只提高等待型请求下 Worker 线程的利用率,不减少 CPU 计算量,也不让慢下游更快返回。
- 误区:调用
startAsync()后就不用线程了。 后续业务仍要在业务线程池或异步 I/O 回调里执行,只是不长期占用 Tomcat Worker。 - 追问:为什么必须调用
complete()?complete()标志异步请求结束并释放相关资源,遗漏后请求会悬挂直到超时甚至造成资源泄漏。 - 追问:异步 Servlet 适合长轮询的原因是什么? 长轮询大量时间在等事件,异步能让连接保持等待而不长期占用 Worker 线程。
- 误区:异步后就不需要超时。 下游永久不返回时,异步请求、连接和上下文对象仍会堆积,超时和
AsyncListener是生产必需品。 - 追问:CPU 密集任务该怎么优化? 应优先优化算法、拆分任务、控制并发或扩容 CPU,而不是指望 Servlet 异步降低计算时间。
九、加强记忆
Servlet 3 异步(startAsync())释放 Tomcat Worker 线程——发起异步后 Worker 立即归还线程池去服务别的请求,耗时任务交给业务线程池/回调,完成后用 AsyncContext 写响应并 complete()。提升的是「请求线程利用率」,不是单个任务耗时——所以只对「等待型」任务有效(长轮询、慢 I/O、等回调),CPU 密集任务无效甚至有害(计算量没少、多了调度开销)。不能提高所有请求性能。必须配套:超时(setTimeout)+ AsyncListener(onTimeout/onError/onComplete)+ 业务线程池 + complete()。误区:异步不等于不用线程(只是不占 Worker,仍要业务线程池);忘设超时/忘 complete 会导致请求悬挂资源泄漏。核心一句:异步把「等待」从 Worker 线程上挪走,不让线程干等,但等的时间没变短。