下面我们来看看另外一种实现,利用Servlet API 3.0的异步支持:
@WebServlet(asyncSupported = true, value = "/AsyncServlet") public class AsyncServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Work.add(request.startAsync()); } } public class Work implements ServletContextListener { private static final BlockingQueue queue = new LinkedBlockingQueue(); private volatile Thread thread; public static void add(AsyncContext c) { queue.add(c); } @Override public void contextInitialized(ServletContextEvent servletContextEvent) { thread = new Thread(new Runnable() { @Override public void run() { while (true) { try { Thread.sleep(2000); AsyncContext context; while ((context = queue.poll()) != null) { try { ServletResponse response = context.getResponse(); response.setContentType("text/plain"); PrintWriter out = response.getWriter(); out.printf("Thread %s completed the task", Thread.currentThread().getName()); out.flush(); } catch (Exception e) { throw new RuntimeException(e.getMessage(), e); } finally { context.complete(); } } } catch (InterruptedException e) { return; } } } }); thread.start(); } @Override public void contextDestroyed(ServletContextEvent servletContextEvent) { thread.interrupt(); } } |
上面的代码看起来有点复杂,因此在开始分析这个解决方案的细节信息之前,我先来概述一下这个方案:速度上提升了75倍,吞吐量提升了20倍。看到这个结果,你肯定迫不及待地想知道这个示例是如何做到的吧。
这个Servlet本身是非常简单的。需要注意两点,首先是声明Servlet支持异步方法调用:
@WebServlet(asyncSupported = true, value = "/AsyncServlet")
其次,重要的部分实际上是隐藏在下面这行代码调用中的。
Work.add(request.startAsync());
整个请求处理都被委托给了Work类。请求上下文是通过AsyncContext实例来保存的,它持有容器提供的请求与响应对象。
现在来看看第2个,也是更加复杂的类,Work类实现了ServletContextListener接口。进来的请求会在该实现中排队等待通知,通知可能是上面提到的拍卖中的竞标价,或是所有请求都在等待的群组聊天中的下一条消息。
当通知到达时,我们这里依然是通过Thread.sleep()让线程睡眠2,000ms,队列中所有被阻塞的任务都是由一个工作线程来处理的,该线程负责编辑与发送响应。相对于阻塞成百上千个线程以等待外部通知,我们通过一种更加简单且干净的方式达成所愿,通过批处理在单独的线程中处理请求。
还是让结果来说话吧,测试配置与方才的示例一样,依然使用Tomcat 7.0.24的默认配置,测试结果如下所示:
平均响应时间:265ms
最快响应时间:6ms
最慢响应时间:2,058ms
吞吐量:1,965个请求/秒
虽然说这个示例很简单,不过对于实际项目来说通过这种方式依然能获得类似的结果。
在将所有的Servlet改写为异步Servlet前,请容许我多说几句。该解决方案非常适合于某些应用场景,比如说群组通知与拍卖价格通知等。不过,对于等待数据库查询完成的请求来说,这种方式就没有什么必要了。像往常一样,我必须得重申一下——请通过实验进行度量,而不是瞎猜。
对于那些不适合于这种解决方案的场景来说,我还是要说一下这种方式的好处。除了在吞吐量与延迟方面带来的显而易见的改进外,这种方式还可以在大负载的情况下优雅地避免可能出现的线程饥饿问题。
另一个重要的方面,这种异步处理请求的方式已经是标准化的了。它不依赖于你所使用的Servlet API 3.0,兼容于各种应用服务器,如Tomcat 7、JBoss 6或是Jetty 8等,在这些服务器上这种方式都可以正常使用。你不必再面对各种不同的Comet实现或是依赖于平台的解决方案了,比如说Weblogic FutureResponseServlet。
就如本文一开始所提的那样,现在的Java Web项目很少会直接使用Servlet API进行开发了,不过诸多的Web MVC框架都是基于Servlet与JSP标准实现的,那么在你的日常开发中,是否使用过出现多年的Servlet API 3.0,使用了它的哪些特性与API呢?