Servlet特性研究之異步模式

Servlet只有同步模型是怎樣的?

異步處理是Servlet3.0版本的重要功能之一,分析異步處理模型之前,先看看同步處理的過程是怎樣的:

  • 客戶端發起HTTP請求一個動態Servlet API,請求到達服務器端後經過靜態服務器過濾後轉交給Servlet容器,
  • 容器從主線程池獲取一個線程,開始執行Servlet程序,執行結束後得到了完整的響應內容並將其返回給調用方
  • 然後將該線程還回線程池,整個過程都是由同一個主線程在執行。

上述模型存在什麼問題呢?

  • 作爲服務器要想提高併發性能,就只能通過提高線程池的最大線程數。即吞吐量瓶頸受線程池約束。
  • 更嚴重的問題是:Servlet執行期間始終佔用了該線程池線程,假如某個高頻且耗時的Servlet被請求,那就會佔用大量甚至全部的線程池線程,進而導致沒有線程來處理其它請求。

什麼是異步處理模式?

相對於前面的同步處理,異步處理的核心本質就是提供了一個突破點:

讓Servlet程序能夠將這些慢操作分配給新線程來執行,同時儘快將該Servlet所佔用的線程歸還到容器線程池。

先來看看異步模式的Servlet程序是怎麼樣的:

import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;

@WebServlet(urlPatterns = "/asyncapi", asyncSupported = true)
public class AsyncServlet extends HttpServlet {
    public void service(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        response.setContentType("text/html;charset=GBK");
        PrintWriter printWriter = response.getWriter();
        printWriter.println("<title>異步Servlet示例</title>");
        printWriter.println("進入Servlet的時間:" + new Date() + "<br/>");
        printWriter.println("執行Servlet的線程ID:" + Thread.currentThread().getId() + " Name=" + Thread.currentThread().getName() + "<br/>");

        // 創建AsyncContext,開始異步調用
        final AsyncContext asyncContext = request.startAsync();
        asyncContext.setTimeout(50 * 1000);// 設置異步調用的超時時長,限制HTTP響應的最大耗時
        asyncContext.start(
                new Runnable() {//創建新線程繼續執行
                    public void run() {
                        try {
                            Thread.sleep(1 * 1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        printWriter.println("執行業務處理的線程ID:" + Thread.currentThread().getId() + " Name=" + Thread.currentThread().getName() + "<br/>");
                        ServletResponse response = asyncContext.getResponse();
                        /* ... print to the response ... */
                        printWriter.println("請求處理結束:" + new Date() + "<br/>");
                        asyncContext.complete();
                    }
                }
        );
        printWriter.println("退出Servlet的時間:" + new Date() + "<br/>");
    }
}

請求該API,根據返回值可以看到現象:

  • Servlet內部模擬耗時了1秒,postman監測到實際耗時也是1秒
  • 處理該次HTTP請求期間,共使用了兩個線程
  • 進入和離開Servlet的時間一致,說明該線程佔用很短暫
  • 執行業務處理的耗時操作佔用了另外一個線程,且執行完成後才返回HTTP響應

至此,我們通過演示程序確實看到使用異步模式將Servet部分和耗時操作部分分配到了不同的線程執行。那麼耗時操作用到的新線程來資源哪裏?

異步處理模式的實現原理

根據文檔描述,start()方法是啓用新線程的關鍵,它是怎麼實現的呢?扒拉源碼看看吧

這需要我們查看Servlet容器的具體實現來驗證,此處以Tomcat 9.0源碼爲例進行分析。

下載好源碼後,首選找到接口AsyncContext及實現類AsyncContextImpl的源碼:

上圖看到tomcat的狀態機進行調度後續的處理。

我們根據狀態值檢索到後續入口:

最終看到是通過executor來執行runnable程序,而executor即tomcat中可配置的連接池。

當然也不是隻能使用start()來啓用新線程,我們甚至可以手工new線程來執行耗時操作,演示如下:

import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;

@WebServlet(urlPatterns = "/asyncapi_newthread", asyncSupported = true)
public class AsyncServlet_NewThread extends HttpServlet {
    public void service(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        response.setContentType("text/html;charset=GBK");
        PrintWriter printWriter = response.getWriter();
        printWriter.println("<title>異步Servlet示例</title>");
        printWriter.println("進入Servlet的時間:" + new Date() + "<br/>");
        printWriter.println("執行Servlet的線程ID:" + Thread.currentThread().getId() + " Name=" + Thread.currentThread().getName() + "<br/>");

        // 創建AsyncContext,開始異步調用
        final AsyncContext asyncContext = request.startAsync();
        asyncContext.setTimeout(50 * 1000);// 設置異步調用的超時時長,限制HTTP響應的最大耗時

        new Thread(() -> {
            try {
                Thread.sleep(1 * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            printWriter.println("執行業務處理的線程ID:" + Thread.currentThread().getId() + " Name=" + Thread.currentThread().getName() + "<br/>");
            ServletResponse aResponse = asyncContext.getResponse();
            /* ... print to the response ... */
            printWriter.println("請求處理結束:" + new Date() + "<br/>");
            asyncContext.complete();
        }).start();
        printWriter.println("退出Servlet的時間:" + new Date() + "<br/>");
    }
}

異步模式帶來的好處是什麼?

從編程方面提供了異步能力,在編程環節就可以提前將耗時較高的Servlet啓用異步模式。

異步模式具備拆分線程池解耦的能力,配置異步模式從專有work線程池取線程,而不是與原Servlet線程池共用線程,可以顯著提高Servlet容器的併發量,減小對其它Serlvet請求的影響。

同時要看到:異步模式只能說讓Tomcat有機會接收更多請求,並不能提升特定服務的吞吐量。

參考資料:

Java EE7官方文檔目錄:https://docs.oracle.com/javaee/7/tutorial/index.html

Servlet的異步處理:https://docs.oracle.com/javaee/7/tutorial/servlets012.htm

Servlet的非阻塞IO:https://docs.oracle.com/javaee/7/tutorial/servlets013.htm

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章