tomcat下使用Servlet異步模式的坑坑窪窪

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

相關文章
相關標籤/搜索