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