← 返回题目列表

Servlet 3 异步处理是什么?它能提高所有请求的性能吗?

高频 困难 第 11 / 23 题 更新于 2026/07/25
AsyncContextServlet异步线程模型

简化版

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,等待 800msWorker 陪着等 800msWorker 发起异步后归还,等待放到业务线程/回调
长轮询,最多等 30sWorker 可能被挂住 30s连接保留,请求线程不长期占用
CPU 加密计算 800msWorker 算 800ms换线程仍要算 800ms,收益很小

记忆钩子:Servlet 异步省的是“Tomcat Worker 陪等的时间”,不是把下游 I/O 或 CPU 计算本身变短。

二、异步处理的流程:把「等待」从 Worker 线程上挪走

Servlet 3 异步就是解决这个浪费:

  1. 请求进入 Servlet 后,调用 request.startAsync(),请求进入异步模式,返回一个 AsyncContext
  2. 此时 Worker 线程可以立即返回线程池——它不用再等这个请求处理完,可以去接别的请求。
  3. 这个请求的耗时操作交给其他线程(业务线程池)或异步 I/O 回调去做。
  4. 耗时操作完成后,通过 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 线程上挪走,不让线程干等,但等的时间没变短