非同步servlet是servlet3.0開始支援的,對於單次訪問來講,同步的servlet相比非同步的servlet在響應時長上並不會帶來變化(這也是常見的誤區之一),但對於高併發的服務而言非同步servlet能增加服務端的吞吐量。本篇來從原始碼角度上來探究為何說非同步servlet能增加服務端的吞吐量的?
首先來個簡單的非同步servlet的demo
@WebServlet(
name = "asynchelloServlet",
urlPatterns = {"/asynchello"},
asyncSupported = true
)
public class AsyncHelloServlet extends HttpServlet {
private static final ThreadPoolExecutor executor;
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
AsyncContext ctx = req.startAsync();
executor.execute(() -> {
System.out.println("AsyncHello Start->" + LocalDateTime.now());
try {
PrintWriter writer = ctx.getResponse().getWriter();
writer.write("asyncHelloWorld");
} catch (IOException var2) {
var2.printStackTrace();
}
ctx.complete();
System.out.println("AsyncHello End->" + LocalDateTime.now());
});
}
static {
executor = new ThreadPoolExecutor(10, 20, 5000L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue(100));
}
}
上面的程式碼寫非同步servlet的寫法最關鍵的就是
AsyncContext ctx = req.startAsync() ctx.complete()
我們先講講下當邏輯進入servlet之前,tomcat經歷了哪些步驟:
tcp三次握手後 Acceptor執行緒處理 socket accept Acceptor執行緒處理 註冊registered OP_READ到多路複用器 ClientPoller執行緒 監聽多路複用器的事件(OP_READ)觸發 從tomcat的work執行緒池取一個工作執行緒來處理socket[http-nio-8080-exec-xx],下面幾個步驟也都是在work執行緒中進行處理的 因為是http協議所以用Http11Processor來解析協議 CoyoteAdapter來適配包裝成Request和Response物件 開始走pipeline管道(Valve),最後一個invoke的是把我們的servlet物件包裝的StandardWrapperValve管道
接下來就走到我們的servlet,由於是我們是非同步的servlet,
1. req.startAsync()
@Override
public AsyncContext startAsync(ServletRequest request,
ServletResponse response) {
if (!isAsyncSupported()) {
IllegalStateException ise =
new IllegalStateException(sm.getString("request.asyncNotSupported"));
log.warn(sm.getString("coyoteRequest.noAsync",
StringUtils.join(getNonAsyncClassNames())), ise);
throw ise;
}
if (asyncContext == null) {
asyncContext = new AsyncContextImpl(this);
}
//修改狀態機
asyncContext.setStarted(getContext(), request, response,
request==getRequest() && response==getResponse().getResponse());
asyncContext.setTimeout(getConnector().getAsyncTimeout());
return asyncContext;
}
從這裡開始有2個執行緒我們要特別關注它們分別做了哪些事情:
tomcat的work執行緒 我們自定義的業務執行緒
當在tomcat的work執行緒中呼叫startAsync(),會建立了一個非同步的上下文(AsyncContext),並且非同步的上下文(AsyncContext)會設定這個狀態機狀態為 STARTING, 然後把這個非同步上下文放到了我們的自定義執行緒池中去執行,
對於非同步的servlet,有一個專門的狀態機來控制:AsyncMachine,如下圖

那狀態機的扭轉控制肯定也做針對非同步做了什麼特殊處理

這裡是一個Socket狀態的切換的處理邏輯,在非同步servlet的時候是通過AsyncMachined的狀態來連動Socket狀態
如上圖非同步狀態機的切換過程為:
DISPATCHED(初始)->STARTING->STARTED->COMPLETING
Socket的狀態的切換為:LONG
對於tomcat的work執行緒而言,servlet呼叫就結束了! 正常來說,如果是同步servlet的話,request和response會在servlet執行完成後由tomcat釋放掉!

非同步的話 在這個時機request和response肯定不能釋放掉,釋放那不就沒得完了!
雖然Request和Response沒有釋放,但是這根work執行緒回到tomcat的執行緒池中去了(非核心執行緒的話那就釋放)。
2. ctx.complete()
回到我們的業務執行緒,處理完業務邏輯後,呼叫ctx.complete()
@Override
public void complete() {
if (log.isDebugEnabled()) {
logDebug("complete ");
}
check();
//更改非同步狀態機
request.getCoyoteRequest().action(ActionCode.ASYNC_COMPLETE, null);
}
注意:COMPLETING是在我們的自定義的業務執行緒改變的!
修改狀態會觸發 新開一個tomcat工作執行緒
非同步狀態狀態切換:
COMPLETING->DISPATCHED
Socket狀態切換為ASYNC_END
如下圖,一次非同步的完整過程如下圖:

總結
研究了整個如何非同步的過程,雖然這個狀態機的切換挺繞的,會發現在非同步servlet中,最大的改變是為了儘快的釋放tomcat的work執行緒,讓它有機會請求新accept過來的請求,接受更多的請求,當在自定義執行緒池中處理好業務邏輯後,在去啟動新的tomcat的work執行緒來處理response,這樣不就很好理解了為什麼說非同步servlet能增加服務端的吞吐量了對吧!
思考:
SpringBoot的@EnableAsync背後是非同步servlet嗎?
servlet 3.1的non-blocking I/O 解決了3.0的什麼問題?
關注公眾號一起學習