异步处理
Servlet 3.0 异步处理:提升 Web 应用并发能力的关键机制
在传统的 Servlet 处理模型中,一个请求会占用容器线程直到响应完成,若遇到耗时操作(如数据库查询、远程接口调用),线程会被长期阻塞,导致线程资源耗尽,并发能力下降。Servlet 3.0 引入异步处理机制,允许线程在启动耗时操作后立即返回容器,待操作完成后再通过异步上下文处理响应,显著提升了资源利用率和并发吞吐量。本文将详细解析异步处理的原理、核心 API 及实践方式。
异步处理的核心原理
传统同步处理的问题
在同步处理中,容器为每个请求分配一个线程,线程会全程参与请求处理(接收请求→业务处理→生成响应)。若业务处理包含耗时操作(如等待数据库返回),线程会处于阻塞状态,无法处理其他请求,导致:
- 线程资源浪费(阻塞时不做有效工作);
- 高并发下线程池耗尽,新请求被拒绝。
异步处理的改进
异步处理允许线程在启动耗时操作后立即释放,返回容器处理其他请求;待耗时操作完成后,再通过新的线程(或回调)处理响应。流程如下:
- 容器线程接收请求,启动异步处理,获取
AsyncContext对象; - 容器线程将耗时操作(如数据库查询)委托给后台线程,自身立即返回容器;
- 后台线程完成耗时操作后,通过
AsyncContext生成响应,结束异步处理。
核心价值:减少线程阻塞时间,提高线程利用率,支持更高的并发请求。
异步处理的核心 API
Servlet 3.0 通过 ServletRequest 和 AsyncContext 接口提供异步处理能力,核心类和方法如下:
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+)
// 关键配置
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,否则异步处理会失败。
异步处理的实现示例
基础示例:处理耗时操作
public class AsyncServlet extends HttpServlet {
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() {
public void onComplete(AsyncEvent event) throws IOException {
System.out.println("异步处理完成");
}
public void onTimeout(AsyncEvent event) throws IOException {
event.getAsyncContext().getResponse().getWriter().write("处理超时");
event.getAsyncContext().complete(); // 结束异步
}
public void onError(AsyncEvent event) throws IOException {
System.out.println("异步处理错误:" + event.getThrowable());
}
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()设置合理超时时间,避免客户端长期等待;超时后需在监听器中手动结束异步。 - 线程安全:
AsyncContext的getRequest()和getResponse()在多线程环境下需注意线程安全,避免并发修改。 - 过滤器兼容:若请求经过过滤器,过滤器需配置
asyncSupported=true,否则会抛出IllegalStateException。 - 响应提交:异步处理中,响应对象的状态由容器管理,避免手动调用
response.flushBuffer()或response.close()。
异步处理 vs 同步处理
| 特性 | 同步处理 | 异步处理 |
|---|---|---|
| 线程占用 | 全程占用线程(直到响应完成) | 仅启动阶段占用线程,耗时操作期间释放 |
| 并发能力 | 低(线程易阻塞) | 高(线程利用率高) |
| 适用场景 | 短耗时操作(如简单计算) | 长耗时操作(如远程调用、IO 操作) |
| 复杂度 | 低(直接处理) | 高(需管理异步生命周期) |