暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Tomcat中的Servlet异步是怎么实现的?(Wrapper系列之三)

中间件技术讨论圈 2016-08-10
467

在前面分析过的Servlet3.1规范中,我们已经就Servlet3.1异步已经描述过,但仅仅是浅析了Tomcat的实现,给出了一些示意图,并没有通过源码分析异步调用的整个过程。


1.Servlet3.1异步示例


再回顾一下例子:

  1. @WebServlet(urlPatterns="/demo"asyncSupported=true)  

  2. public class AsynServlet extends HttpServlet {  

  3.     private static final long serialVersionUID = -8016328059808092454L;  

  4.       

  5.     /* (non-Javadoc) 

  6.      * @see javax.servlet.http.HttpServlet#service(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) 

  7.      */  

  8.     @Override  

  9.     protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {  

  10.             resp.setContentType("text/html;charset=UTF-8");  

  11.             PrintWriter out = resp.getWriter();  

  12.             out.println("进入Servlet的时间:" + new Date() + ".");  

  13.             out.flush();  

  14.   

  15.             //在子线程中执行业务调用,并由其负责输出响应,主线程退出  

  16.             final AsyncContext ctx = req.startAsync();  

  17.             ctx.setTimeout(200000);  

  18.             new Work(ctx).start();  

  19.             out.println("结束Servlet的时间:" + new Date() + ".");  

  20.             out.flush();  

  21.     }  

  22. }  

  23.   

  24. class Work extends Thread{  

  25.     private AsyncContext context;  

  26.       

  27.     public Work(AsyncContext context){  

  28.         this.context = context;  

  29.     }  

  30.     @Override  

  31.     public void run() {  

  32.         try {  

  33.             Thread.sleep(2000);//让线程休眠2s钟模拟超时操作  

  34.             PrintWriter wirter = context.getResponse().getWriter();           

  35.             wirter.write("延迟输出");  

  36.             wirter.flush();  

  37.             context.complete();  

  38.         } catch (InterruptedException e) {  

  39.               

  40.         } catch (IOException e) {  

  41.               

  42.         }  

  43.     }  

红色的代码部分为非常关键的步骤。


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上下文 的缓存机制,进行跳转和结果的返回。


文章转载自中间件技术讨论圈,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论