servlet3.0版本之后,增长了对异步模式的支持。java
以往在servlet里面,每个新的请求到来都会由一个线程来接收处理,在处理过程当中若是须要等待其余操做的结果,则线程就会处于阻塞状态不能执行其余任务,待任务结束后该线程将结果输出给客户端,这时该线程才能继续处理其余的请求。为了提升线程利用效率,servlet3.0版本之后增长了异步处理请求的模式,容许当前线程将任务提交到给其余后台线程处理(通常是后台线程池,这样只须要较少的线程就能够处理大量的任务),自身转而去接收新的请求。apache
protected void service(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException { response.getWriter().println("hello"); }
要使用异步模式,只须要调用request对象的startAsync方法便可,该方法返回一个AsyncContext对象供后续使用,能够经过该对象设置异步处理的超时间,添加异步处理的监听器等。而后将要处理的任务提交到某个线程池,当前线程执行完后续的代码后就能去处理其余新的请求,不用等待当前任务执行完。当前任务交由后台线程池执行完后,能够调用asyncContext.complete方法表示任务处理完成,触发以前添加的监听器对事件进行响应。tomcat
protected void service(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException { //启用异步模式 final AsyncContext ac = request.startAsync(); //超时设置 ac.setTimeout(1000L); //添加监听器便于观察发生的事件 ac.addListener(new AsyncListener() { @Override public void onComplete(AsyncEvent asyncEvent) throws IOException { System.out.println("onComplete"); } @Override public void onTimeout(AsyncEvent asyncEvent) throws IOException { System.out.println("onTimeout"); } @Override public void onError(AsyncEvent asyncEvent) throws IOException { System.out.println("onError"); } @Override public void onStartAsync(AsyncEvent asyncEvent) throws IOException { System.out.println("onStartAsync"); } }); executor.submit(new Runnable() { @Override public void run() { //这里可使用request, response,ac等对象 try { String user = request.getParameter("user"); response.getWriter().println("hello from async " + user); ac.complete(); } catch (IOException e) { e.printStackTrace(); } } }); //方法结束当前线程能够去处理其余请求了 }
因为asyncContext对象中持有请求中的request和response对象,因此在任务异步执行完后仍然能够经过response将结果输出给客户端。可是,tomcat在通过超时间以后还未收到complete消息,会认为异步任务已经超时,须要结束当前的请求,从而将response对象放回对象池供其余请求继续使用。这时response对象会分配给新的请求使用,按理就不该该再被以前的异步任务共用!可是异步任务自己并不知道任务已经超时了,还在继续运行,所以还会使用response对象进行输出,这时就会发生新的请求与后台异步任务共同一个resonse对象的现象!这会形成多个线程向同一个客户端输出结果,将本不是该客户端须要的结果输出。试想一下:原本请求是的查询个人订单列表,结果收到了别人的订单列表,这个后果是否是很严重呢?bash
为验证这个问题,可使用如下代码进行测试:curl
package async; import javax.servlet.AsyncContext; import javax.servlet.AsyncEvent; import javax.servlet.AsyncListener; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class AsyncTimeoutServlet extends HttpServlet { boolean running = false; boolean stop = false; ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 50000L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(100)); @Override public void init() throws ServletException { System.out.println("init AsyncTimeoutServlet"); } @Override public void destroy() { executor.shutdownNow(); } protected void service(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException { stop = "true".equals(request.getParameter("stop")); //这里只对第一次请求使用异步模式,后续请求均使用同步模式 if (running) { System.out.println("running"); try { //在同步模式下输出response对象的hashcode response.getWriter().println("this response belong's to you:" + response.toString()); } catch (IOException e) { System.out.println("response error"); } return; } running = true; //启用异步模式 final AsyncContext ac = request.startAsync(); System.out.println("startAsync"); //超时设置为1s便于快速超时 ac.setTimeout(1000L); //添加监听器便于观察发生的事件 ac.addListener(new AsyncListener() { @Override public void onComplete(AsyncEvent asyncEvent) throws IOException { System.out.println("onComplete"); } @Override public void onTimeout(AsyncEvent asyncEvent) throws IOException { System.out.println("onTimeout"); } @Override public void onError(AsyncEvent asyncEvent) throws IOException { System.out.println("onError"); } @Override public void onStartAsync(AsyncEvent asyncEvent) throws IOException { System.out.println("onStartAsync"); } }); executor.submit(new Runnable() { @Override public void run() { while (!stop) { try { //每隔3s向原始的response对象中输出结果,便于客户端观察是否有收到该结果 Thread.sleep(3000L); System.out.println("async run"); try { response.getWriter().println("if you see this message, something must be wrong. I'm " + response.toString()); } catch (IOException e) { System.out.println("async response error"); } } catch (InterruptedException e) { e.printStackTrace(); return; } } System.out.println("stop"); } }); System.out.println("ok, async mode started."); } }
在上面的测试示例中,咱们对第一次请求开启了异步模式,后续的请求仍然采用同步模式,并只是简单地输出response对象的hashcode,将一个任务提交到了线程池中运行。在异步任务里每隔3s向客户端输出一次response对象的hashcode,而这个response对象是第一个请求的response对象,也就是说,它应该与后续的请求使用了不一样的response对象才对。可是在屡次调用该servlet后,有些请求获得的结果中包含了第一次请求时产生的异步任务中输出的内容,也就是后续的有些请求与第一次请求共用了同一个response对象,tomcat对response对象进行了重用!异步
测试结果以下:async
curl -i "http://127.0.0.1:8080/servlet_async/async" HTTP/1.1 200 OK Server: Apache-Coyote/1.1 Content-Length: 192 Date: Wed, 21 Aug 2019 07:55:26 GMT if you see this message, something must be wrong. I'm org.apache.catalina.connector.ResponseFacade@51582d92 this response belong's to you:org.apache.catalina.connector.ResponseFacade@51582d92
并非每一次请求都能成功重用到同一个response,因此上述请求有可能须要运行屡次才能出现预期的结果。ide
避坑方法:测试
异步任务若是须要使用response对象,先判断当前异步模式是否已经超时和结束了,若是结束了则不要再使用该对象,使用request对象也是同理。不过,有时候咱们会把request对象传入异步任务,在任务执行的时候会从中取出一些数据使用,好比getParameter获取参数,这种状况下能够事先从request对象中获取到异步任务须要的全部数据,封装成新的对象供异步任务使用,避免使用tomcat提供的request对象。this