詳解Servlet 3.0/3.1 中的異步處理
在Servlet 3.0之前,Servlet采用Thread-Per-Request的方式處理請求,即每一次Http請求都由某一個線程從頭到尾負責處理。如果一個請求需要進行IO操作,比如訪問數(shù)據(jù)庫、調(diào)用第三方服務(wù)接口等,那么其所對應的線程將同步地等待IO操作完成, 而IO操作是非常慢的,所以此時的線程并不能及時地釋放回線程池以供后續(xù)使用,在并發(fā)量越來越大的情況下,這將帶來嚴重的性能問題。即便是像Spring、Struts這樣的高層框架也脫離不了這樣的桎梏,因為他們都是建立在Servlet之上的。為了解決這樣的問題,Servlet 3.0引入了異步處理,然后在Servlet 3.1中又引入了非阻塞IO來進一步增強異步處理的性能。
本文源代碼:https://github.com/davenkin/servlet-3-async-learning
項目下載地址:servlet-3-async-learning_jb51.rar
在Servlet 3.0中,我們可以從HttpServletRequest對象中獲得一個AsyncContext對象,該對象構(gòu)成了異步處理的上下文,Request和Response對象都可從中獲取。AsyncContext可以從當前線程傳給另外的線程,并在新的線程中完成對請求的處理并返回結(jié)果給客戶端,初始線程便可以還回給容器線程池以處理更多的請求。如此,通過將請求從一個線程傳給另一個線程處理的過程便構(gòu)成了Servlet 3.0中的異步處理。
舉個例子,對于一個需要完成長時處理的Servlet來說,其實現(xiàn)通常為:
@WebServlet("/syncHello")
public class SyncHelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
new LongRunningProcess().run();
response.getWriter().write("Hello World!");
}
}
為了模擬長時處理過程,我們創(chuàng)建了一個LongRunningProcess類,其run()方法將隨機地等待2秒之內(nèi)的一個時間:
public class LongRunningProcess {
public void run() {
try {
int millis = ThreadLocalRandom.current().nextInt(2000);
String currentThread = Thread.currentThread().getName();
System.out.println(currentThread + " sleep for " + millis + " milliseconds.");
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
此時的SyncHelloServlet將順序地先執(zhí)行LongRunningProcess的run()方法,然后將將HelloWorld返回給客戶端,這是一個典型的同步過程。
在Servlet 3.0中,我們可以這么寫來達到異步處理:
@WebServlet(value = "/simpleAsync", asyncSupported = true)
public class SimpleAsyncHelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
AsyncContext asyncContext = request.startAsync();
asyncContext.start(() -> {
new LongRunningProcess().run();
try {
asyncContext.getResponse().getWriter().write("Hello World!");
} catch (IOException e) {
e.printStackTrace();
}
asyncContext.complete();
});
}
此時,我們先通過request.startAsync()獲取到該請求對應的AsyncContext,然后調(diào)用AsyncContext的start()方法進行異步處理,處理完畢后需要調(diào)用complete()方法告知Servlet容器。start()方法會向Servlet容器另外申請一個新的線程(可以是從Servlet容器中已有的主線程池獲取,也可以另外維護一個線程池,不同容器實現(xiàn)可能不一樣),然后在這個新的線程中繼續(xù)處理請求,而原先的線程將被回收到主線程池中。事實上,這種方式對性能的改進不大,因為如果新的線程和初始線程共享同一個線程池的話,相當于閑置下了一個線程,但同時又占用了另一個線程。
當然,除了調(diào)用AsyncContext的start()方法,我們還可以通過手動創(chuàng)建線程的方式來實現(xiàn)異步處理:
@WebServlet(value = "/newThreadAsync", asyncSupported = true)
public class NewThreadAsyncHelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
AsyncContext asyncContext = request.startAsync();
Runnable runnable = () -> {
new LongRunningProcess().run();
try {
asyncContext.getResponse().getWriter().write("Hello World!");
} catch (IOException e) {
e.printStackTrace();
}
asyncContext.complete();
};
new Thread(runnable).start();
}
}
自己手動創(chuàng)建新線程一般是不被鼓勵的,并且此時線程不能重用。因此,一種更好的辦法是我們自己維護一個線程池。這個線程池不同于Servlet容器的主線程池,如下圖:

在上圖中,用戶發(fā)起的請求首先交由Servlet容器主線程池中的線程處理,在該線程中,我們獲取到AsyncContext,然后將其交給異步處理線程池??梢酝ㄟ^Java提供的Executor框架來創(chuàng)建線程池:
@WebServlet(value = "/threadPoolAsync", asyncSupported = true)
public class ThreadPoolAsyncHelloServlet extends HttpServlet {
private static ThreadPoolExecutor executor = new ThreadPoolExecutor(100, 200, 50000L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(100));
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
AsyncContext asyncContext = request.startAsync();
executor.execute(() -> {
new LongRunningProcess().run();
try {
asyncContext.getResponse().getWriter().write("Hello World!");
} catch (IOException e) {
e.printStackTrace();
}
asyncContext.complete();
});
}
}
Servlet 3.0對請求的處理雖然是異步的,但是對InputStream和OutputStream的IO操作卻依然是阻塞的,對于數(shù)據(jù)量大的請求體或者返回體,阻塞IO也將導致不必要的等待。因此在Servlet 3.1中引入了非阻塞IO(參考下圖紅框內(nèi)容),通過在HttpServletRequest和HttpServletResponse中分別添加ReadListener和WriterListener方式,只有在IO數(shù)據(jù)滿足一定條件時(比如數(shù)據(jù)準備好時),才進行后續(xù)的操作。

對應的代碼示:
@WebServlet(value = "/nonBlockingThreadPoolAsync", asyncSupported = true)
public class NonBlockingAsyncHelloServlet extends HttpServlet {
private static ThreadPoolExecutor executor = new ThreadPoolExecutor(100, 200, 50000L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(100));
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
AsyncContext asyncContext = request.startAsync();
ServletInputStream inputStream = request.getInputStream();
inputStream.setReadListener(new ReadListener() {
@Override
public void onDataAvailable() throws IOException {
}
@Override
public void onAllDataRead() throws IOException {
executor.execute(() -> {
new LongRunningProcess().run();
try {
asyncContext.getResponse().getWriter().write("Hello World!");
} catch (IOException e) {
e.printStackTrace();
}
asyncContext.complete();
});
}
@Override
public void onError(Throwable t) {
asyncContext.complete();
}
});
}
}
在上例中,我們?yōu)镾ervletInputStream添加了一個ReadListener,并在ReadListener的onAllDataRead()方法中完成了長時處理過程。
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
SpringCloud實現(xiàn)全鏈路灰度發(fā)布的示例詳解
灰度發(fā)布是指在軟件或服務(wù)發(fā)布過程中,將新版本的功能或服務(wù)以較小的比例引入到生產(chǎn)環(huán)境中,僅向部分用戶或節(jié)點提供新功能的一種發(fā)布策略,下面我們就來學習一下SpringCloud如何實現(xiàn)全鏈路灰度發(fā)布2023-11-11
Java Swing組件單選框JRadioButton用法示例
這篇文章主要介紹了Java Swing組件單選框JRadioButton用法,結(jié)合具體實例形式分析了Swing單選框JRadioButton的使用方法及相關(guān)操作注意事項,需要的朋友可以參考下2017-11-11
劍指Offer之Java算法習題精講二叉樹的構(gòu)造和遍歷
跟著思路走,之后從簡單題入手,反復去看,做過之后可能會忘記,之后再做一次,記不住就反復做,反復尋求思路和規(guī)律,慢慢積累就會發(fā)現(xiàn)質(zhì)的變化2022-03-03
struts2與cookie 實現(xiàn)自動登錄和驗證碼驗證實現(xiàn)代碼
這篇文章主要介紹了struts2與cookie 實現(xiàn)自動登錄和驗證碼驗證實現(xiàn)代碼的相關(guān)資料,需要的朋友可以參考下2016-10-10
SpringMVC前端和后端數(shù)據(jù)交互總結(jié)
本篇文章主要介紹了SpringMVC前端和后端數(shù)據(jù)交互總結(jié),具有一定的參考價值,感興趣的小伙伴們可以參考一下。2017-03-03
Java?SSM實現(xiàn)前后端協(xié)議聯(lián)調(diào)詳解下篇
首先我們已經(jīng)知道,在現(xiàn)在流行的“前后端完全分離”架構(gòu)中,前后端聯(lián)調(diào)是一個不可能避免的問題,這篇文章主要介紹了Java?SSM實現(xiàn)前后端協(xié)議聯(lián)調(diào)過程2022-08-08

