在前面分析过的Servlet3.1规范中,我们已经就Servlet3.1异步已经描述过,但仅仅是浅析了Tomcat的实现,给出了一些示意图,并没有通过源码分析异步调用的整个过程。
1.Servlet3.1异步示例
再回顾一下例子:
@WebServlet(urlPatterns="/demo", asyncSupported=true)
public class AsynServlet extends HttpServlet {
private static final long serialVersionUID = -8016328059808092454L;
/* (non-Javadoc)
* @see javax.servlet.http.HttpServlet#service(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
*/
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=UTF-8");
PrintWriter out = resp.getWriter();
out.println("进入Servlet的时间:" + new Date() + ".");
out.flush();
//在子线程中执行业务调用,并由其负责输出响应,主线程退出
final AsyncContext ctx = req.startAsync();
ctx.setTimeout(200000);
new Work(ctx).start();
out.println("结束Servlet的时间:" + new Date() + ".");
out.flush();
}
}
class Work extends Thread{
private AsyncContext context;
public Work(AsyncContext context){
this.context = context;
}
@Override
public void run() {
try {
Thread.sleep(2000);//让线程休眠2s钟模拟超时操作
PrintWriter wirter = context.getResponse().getWriter();
wirter.write("延迟输出");
wirter.flush();
context.complete();
} catch (InterruptedException e) {
} catch (IOException e) {
}
}
}
红色的代码部分为非常关键的步骤。
a.首先标识当前Servlet是异步Servlet,这个通过原注释为asyncSupported 进行标注;
b.我们看到上面的代码通过request获得一个异步的上下文AsyncContext ,这个上下文是启动异步Servlet的关键;
c.需要准备一个业务线程,这个业务线程可能执行时间较长,用来模拟真实的业务,在上个例子中,使用Work类进行模拟,一定要注意将上述的异步的上下文AsyncContext 传入到业务线程中,因为我们要在业务线程执行结束之后,回到Tomcat的工作线程,而这个“回”的过程,就需要这个AsyncContext 提供帮助;
d.业务线程(Work)执行完毕之后,需要调用AsyncContext异步上下文,“指示”Tomcat下一步应该做什么?上述的例子中业务日志完成了,后续就拉倒了,因此直接调用context.complete()告诉Tomcat我的活完了,Tomcat对这个请求做后续处理即可;
而对于一般通常的情况,业务线程处理完之后,需要Tomcat完成页面跳转,这个时候就不能用context.complete() 了,应该使用dispatch:

上图是从Servlet3.1规范中截取下来的,其中每个示例中的上半部分是在Servlet中写的,当有页面跳转时,可以调用request.getRequestDispatcher(..).forward(..);但是在Servlet处理结束后,并不会立刻进行跳转,而是等待业务线程调用异步上下文发出dispatch的指令,当AsyncContext调用dispatch之后,Tomcat的工作线程接下来执行页面跳转,并完成工作线程后续的处理;
2.异步Servlet实现原理
搞出这么一个异步Servlet线程,究竟有什么意义呢?
看看下面这个图,可以比较经典的总结出来:

我们前面研究过Tomcat的前端工作线程池,可以这么讲,是非常繁忙的,特别是并发压力较大,一个简单的请求都甚至可以开启一个线程,因此上图中的左侧thread一直是比较繁忙的;
我们经常会遇到一个场景,业务执行非常的慢,而Tomcat的工作线程还要有很多并发请求需要处理,这里就是一个矛盾,有什么办法我能让业务自己去执行,而Tomcat线程去干更多的活呢?
这个就是异步Servlet了,我们从上图的第二个图可以看到,业务线程在干活的时候,Tomcat工作线程这个时候已经释放了,不会干活了,甚至可能已经回到线程池中,该线程从线程池出来又去给其它线程服务了;
而当业务线程干完活之后,通知Tomcat工作线程池,这个时候,从线程池中需要再出来一个工作线程池,把业务的活完了,继续接着干,这个时候Tomcat工作线程继续完成前面没有完成的任务,例如进行跳转,接收结果,清空该请求占用的对象,回收线程....;
总结一句就是,使用异步servlet这种模式,可以大大减轻Tomcat工作线程的负载,当大并发长事务,这种异步servlet的方式可谓是非常有效;
3.Tomcat的实现
我们来看看Tomcat的实现吧,主要关注的就是从Request中获取的上下文AsyncContext 接口,其实现类为AsyncContextImpl
我们跟踪其Request.startAsync的方法进来,看到其直接初始化的就是AsyncContextImpl:

对于这个AsyncContextImpl,其之所以叫做异步Servlet的上下文,其主要是缓存了很多的请求的中间信息,如request,response,listener,dispatch对象,event,还有servlet实例化的instanceManager;
思考一下,AsyncContextImpl之所以缓存这些东西,是因为Tomcat工作线程在Request.startAsync之后,把该异步servlet的后续代码执行完毕后,Tomcat工作线程直接就结束了,也就是返回线程池中了,相当于线程根本不会保存记录信息,而我们这时的状态相当于一个请求到服务器了,业务正在执行,执行到一半,线程没了,这就需要至少有个缓存的地方,而这个缓存恰恰就是这个AsyncContextImpl 。
我们继续看看当业务线程处理完了之后,调用AsyncContextImpl 的complete和dispatch是怎么实现的,先看看比较简单的complete方法:

从业务线程的代码调用AsyncContextImpl ,然后发出ASYNC_COMPLETE事件,该事件显然是观察者模式,是在Tomcat的前端的Http11Processor中注册的n个事件中的一个,当处理器接到该事件后,执行2步操作,第一步是推动异步Servlet状态机,第二步就是重新开启工作线程继续执行下面的任务;
对于第一步的状态机,这个是JAVA EE规范中明确要求实现的,在Tomcat中是AsyncStateMachine类,在该类中,对Servlet出于异步环境中的状态进行监控和管理,对于每一个状态都有严格的要求,其下一个状态做什么,简单看看状态图:

这里就不再对于这些状态每一步的操作进行解释,可以看看Servlet3.1的规范中,对此有非常明确的说明,并且图也比这个要好看的多;
我们主要关注第二步,endpoint就是通道的实现,对于BIO来讲,就是JIOEndpoint,对于NIO来讲,就是NIOEndpoint..,我们就来看BIO的方式:

SocketProcessor是工作线程池,上述的的操作,相当于重新开启一个工作线程,这个工作线程带着SocketWrapper,又来一遍容器的流程,而这一遍的流程,因为Servlet已经处理过,所以会略过servlet的执行直接将后续的处理走完,包括最后response的收尾,对象的清空等等,虽然这里说的比较简单,但可想而知,在整个容器的执行过程中,必须时刻的考虑异步的情况。最终,这次流程走完,服务器端正确返回给浏览器客户端,异步Servlet处理就结束了。
如果是AsyncContextImpl.dispatch的话,就稍微复杂了一些,基于上述的分析,主要是在工作线程在重新走下一次流程的时候,你需要告诉其dispatch到哪个页面了,这就是比complete复杂的地方了。
我们看看AsyncContextImpl.dispatch方法究竟干了啥:

与AsyncContextImpl.complete进行对比,多了前面的两个步骤。
首先是需要通过AsyncContextImpl 缓存的request将要跳转的path找出来,然后直接定义一个线程,该线程中除了状态机的推动,最重要的是Dispatcher.dispatch(request,response),并缓存为dispatch。
这个缓存非常重要,当Tomcat工作线程重新开启之后重走一遍容器流程,当执行到StandardWrapperValve的时候,会判断请求是否是异步dispatch,如果是的话,直接会执行上一次AsyncContextImpl 缓存的dispatch线程,进行跳转:

一直到状态机的推动,最后状态机为完成,服务器端也成功将异步servlet的结果返回到了path的页面中,这就是整个的流程。
对于状态机这里值得提的一句是,AsyncContextImpl 中定义了很多的方法,每当一个状态触发后,这些方法都会被调用,而这些状态的改变的流程是分散在整个容器的调用链条中,在非常多的分支判断中对异步进行了判断。
因而,可以总结的是,异步servlet的实现从原理看起来很容易,但是细节非常琐碎,特别是状态机推动和分支判断,确实不好实现。
本文仅仅以原理性做引导,对细枝末节忽略不计。
总结:
异步Servlet的实现,巧妙的通过AsyncContextImpl上下文来进行实现,在业务线程执行结束之后,Tomcat工作线程重新从池中出来,接管业务线程的结果,并利用AsyncContextImpl上下文 的缓存机制,进行跳转和结果的返回。




