异步处理

Servlet 3.0 异步处理:提升 Web 应用并发能力的关键机制

在传统的 Servlet 处理模型中,一个请求会占用容器线程直到响应完成,若遇到耗时操作(如数据库查询、远程接口调用),线程会被长期阻塞,导致线程资源耗尽,并发能力下降。Servlet 3.0 引入异步处理机制,允许线程在启动耗时操作后立即返回容器,待操作完成后再通过异步上下文处理响应,显著提升了资源利用率和并发吞吐量。本文将详细解析异步处理的原理、核心 API 及实践方式。

异步处理的核心原理

传统同步处理的问题

在同步处理中,容器为每个请求分配一个线程,线程会全程参与请求处理(接收请求→业务处理→生成响应)。若业务处理包含耗时操作(如等待数据库返回),线程会处于阻塞状态,无法处理其他请求,导致:

  • 线程资源浪费(阻塞时不做有效工作);
  • 高并发下线程池耗尽,新请求被拒绝。

异步处理的改进

异步处理允许线程在启动耗时操作后立即释放,返回容器处理其他请求;待耗时操作完成后,再通过新的线程(或回调)处理响应。流程如下:

  1. 容器线程接收请求,启动异步处理,获取 AsyncContext 对象;
  2. 容器线程将耗时操作(如数据库查询)委托给后台线程,自身立即返回容器;
  3. 后台线程完成耗时操作后,通过 AsyncContext 生成响应,结束异步处理。

核心价值:减少线程阻塞时间,提高线程利用率,支持更高的并发请求。

异步处理的核心 API

Servlet 3.0 通过 ServletRequestAsyncContext 接口提供异步处理能力,核心类和方法如下:

ServletRequest 中的异步方法

方法 作用描述
startAsync() 启动异步处理,返回 AsyncContext 对象(使用原始请求和响应)。
startAsync(ServletRequest, ServletResponse) 启动异步处理,使用包装后的请求 / 响应对象(如 ServletRequestWrapper)。
isAsyncStarted() 判断当前请求是否处于异步模式。
isAsyncSupported() 判断当前 Servlet 是否支持异步处理(需配置 asyncSupported=true)。
getAsyncContext() 获取当前请求的 AsyncContext 对象(仅在异步模式下有效)。

AsyncContext 接口(异步上下文)

AsyncContext 是异步处理的核心对象,封装了请求 / 响应的上下文,提供控制异步流程的方法:

方法 作用描述
dispatch() 将请求分派到原始 Servlet 或指定资源(如 JSP),用于异步完成后继续处理。
dispatch(String path) 分派到指定路径的资源(如 /async/result.jsp)。
complete() 结束异步处理,通知容器响应已完成(必须调用,否则响应会一直挂起)。
start(Runnable run) 将任务提交到容器的线程池执行(通常用于处理耗时操作,避免阻塞)。
addListener(AsyncListener listener) 注册异步监听器,监听异步处理的生命周期事件(如超时、完成、错误)。
setTimeout(long timeout) 设置异步处理超时时间(毫秒),超时后触发 AsyncListener.onTimeout()
getRequest() / getResponse() 获取异步上下文关联的请求 / 响应对象。

启用异步处理的配置

在 Servlet 中启用异步

需通过注解或 web.xml 配置 asyncSupported=true,声明 Servlet 支持异步处理:

(1)注解配置(Servlet 3.0+)
@WebServlet(urlPatterns = "/async", asyncSupported = true) // 关键配置
public class AsyncServlet extends HttpServlet {
    // 实现异步处理逻辑
}
(2)web.xml 配置
<servlet>
    <servlet-name>AsyncServlet</servlet-name>
    <servlet-class>com.example.AsyncServlet</servlet-class>
    <async-supported>true</async-supported> <!-- 启用异步 -->
</servlet>
<servlet-mapping>
    <servlet-name>AsyncServlet</servlet-name>
    <url-pattern>/async</url-pattern>
</servlet-mapping>

注意:若 Servlet 调用了过滤器(Filter),过滤器也需配置 asyncSupported=true,否则异步处理会失败。

异步处理的实现示例

基础示例:处理耗时操作

@WebServlet(urlPatterns = "/async", asyncSupported = true)
public class AsyncServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        response.setContentType("text/html;charset=UTF-8");

        // 1. 启动异步处理,获取 AsyncContext
        AsyncContext asyncContext = request.startAsync();
        // 设置超时时间(30秒)
        asyncContext.setTimeout(30000);

        // 2. 注册异步监听器(可选,处理超时、完成等事件)
        asyncContext.addListener(new AsyncListener() {
            @Override
            public void onComplete(AsyncEvent event) throws IOException {
                System.out.println("异步处理完成");
            }

            @Override
            public void onTimeout(AsyncEvent event) throws IOException {
                event.getAsyncContext().getResponse().getWriter().write("处理超时");
                event.getAsyncContext().complete(); // 结束异步
            }

            @Override
            public void onError(AsyncEvent event) throws IOException {
                System.out.println("异步处理错误:" + event.getThrowable());
            }

            @Override
            public void onStartAsync(AsyncEvent event) throws IOException {
                // 新的异步周期启动时触发(较少使用)
            }
        });

        // 3. 提交耗时操作到容器线程池(避免阻塞当前线程)
        asyncContext.start(() -> {
            try {
                // 模拟耗时操作(如数据库查询、远程调用)
                Thread.sleep(5000); // 5秒耗时

                // 4. 处理完成,生成响应
                PrintWriter out = asyncContext.getResponse().getWriter();
                out.write("异步处理完成!耗时:5秒");

                // 5. 结束异步处理(必须调用,否则响应不会返回)
                asyncContext.complete();
            } catch (Exception e) {
                e.printStackTrace();
            }
        });

        // 4. 容器线程在此处立即返回,无需等待耗时操作完成
        System.out.println("容器线程已释放,可处理其他请求");
    }
}

关键流程解析

  • 启动异步request.startAsync() 标记请求进入异步模式,AsyncContext 保存请求 / 响应引用。
  • 耗时操作处理:通过 asyncContext.start(Runnable) 将耗时操作提交到容器线程池,当前 Servlet 线程立即返回。
  • 响应生成:耗时操作完成后,在 Runnable 中通过 asyncContext.getResponse() 获取响应对象,写入结果。
  • 结束异步:调用 asyncContext.complete() 通知容器响应已完成,释放资源。

基于 dispatch 的异步转发

若需将异步处理结果转发到 JSP 渲染,可使用 dispatch() 方法:

asyncContext.start(() -> {
    try {
        // 耗时操作
        Thread.sleep(3000);
        // 设置属性,供 JSP 使用
        asyncContext.getRequest().setAttribute("result", "处理完成");
        // 转发到 result.jsp
        asyncContext.dispatch("/result.jsp");
        // 无需调用 complete(),dispatch 会自动触发
    } catch (Exception e) {
        e.printStackTrace();
    }
});

result.jsp 中可直接获取属性:

<%@ page contentType="text/html;charset=UTF-8" %>
<h1><%= request.getAttribute("result") %></h1>

异步处理的适用场景与注意事项

1. 适用场景

  • 耗时操作:如数据库查询、第三方 API 调用、文件上传 / 下载等。
  • 高并发场景:需要最大化利用线程资源,支持更多并发请求(如秒杀系统、API 网关)。

2. 注意事项

  • 资源释放:必须调用 complete()dispatch() 结束异步处理,否则请求会一直挂起,导致连接泄漏。
  • 超时处理:通过 setTimeout() 设置合理超时时间,避免客户端长期等待;超时后需在监听器中手动结束异步。
  • 线程安全AsyncContextgetRequest()getResponse() 在多线程环境下需注意线程安全,避免并发修改。
  • 过滤器兼容:若请求经过过滤器,过滤器需配置 asyncSupported=true,否则会抛出 IllegalStateException
  • 响应提交:异步处理中,响应对象的状态由容器管理,避免手动调用 response.flushBuffer()response.close()

异步处理 vs 同步处理

特性 同步处理 异步处理
线程占用 全程占用线程(直到响应完成) 仅启动阶段占用线程,耗时操作期间释放
并发能力 低(线程易阻塞) 高(线程利用率高)
适用场景 短耗时操作(如简单计算) 长耗时操作(如远程调用、IO 操作)
复杂度 低(直接处理) 高(需管理异步生命周期)