詳解Servlet 3.0/3.1 中的異步處理
在Servlet 3.0之前,Servlet采用Thread-Per-Request的方式處理請(qǐng)求,即每一次Http請(qǐng)求都由某一個(gè)線(xiàn)程從頭到尾負(fù)責(zé)處理。如果一個(gè)請(qǐng)求需要進(jìn)行IO操作,比如訪(fǎng)問(wèn)數(shù)據(jù)庫(kù)、調(diào)用第三方服務(wù)接口等,那么其所對(duì)應(yīng)的線(xiàn)程將同步地等待IO操作完成, 而IO操作是非常慢的,所以此時(shí)的線(xiàn)程并不能及時(shí)地釋放回線(xiàn)程池以供后續(xù)使用,在并發(fā)量越來(lái)越大的情況下,這將帶來(lái)嚴(yán)重的性能問(wèn)題。即便是像Spring、Struts這樣的高層框架也脫離不了這樣的桎梏,因?yàn)樗麄兌际墙⒃赟ervlet之上的。為了解決這樣的問(wèn)題,Servlet 3.0引入了異步處理,然后在Servlet 3.1中又引入了非阻塞IO來(lái)進(jìn)一步增強(qiáng)異步處理的性能。
本文源代碼:https://github.com/davenkin/servlet-3-async-learning
項(xiàng)目下載地址:servlet-3-async-learning_jb51.rar
在Servlet 3.0中,我們可以從HttpServletRequest對(duì)象中獲得一個(gè)AsyncContext對(duì)象,該對(duì)象構(gòu)成了異步處理的上下文,Request和Response對(duì)象都可從中獲取。AsyncContext可以從當(dāng)前線(xiàn)程傳給另外的線(xiàn)程,并在新的線(xiàn)程中完成對(duì)請(qǐng)求的處理并返回結(jié)果給客戶(hù)端,初始線(xiàn)程便可以還回給容器線(xiàn)程池以處理更多的請(qǐng)求。如此,通過(guò)將請(qǐng)求從一個(gè)線(xiàn)程傳給另一個(gè)線(xiàn)程處理的過(guò)程便構(gòu)成了Servlet 3.0中的異步處理。
舉個(gè)例子,對(duì)于一個(gè)需要完成長(zhǎng)時(shí)處理的Servlet來(lái)說(shuō),其實(shí)現(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!");
}
}
為了模擬長(zhǎng)時(shí)處理過(guò)程,我們創(chuàng)建了一個(gè)LongRunningProcess類(lèi),其run()方法將隨機(jī)地等待2秒之內(nèi)的一個(gè)時(shí)間:
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();
}
}
}
此時(shí)的SyncHelloServlet將順序地先執(zhí)行LongRunningProcess的run()方法,然后將將HelloWorld返回給客戶(hù)端,這是一個(gè)典型的同步過(guò)程。
在Servlet 3.0中,我們可以這么寫(xiě)來(lái)達(dá)到異步處理:
@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();
});
}
此時(shí),我們先通過(guò)request.startAsync()獲取到該請(qǐng)求對(duì)應(yīng)的AsyncContext,然后調(diào)用AsyncContext的start()方法進(jìn)行異步處理,處理完畢后需要調(diào)用complete()方法告知Servlet容器。start()方法會(huì)向Servlet容器另外申請(qǐng)一個(gè)新的線(xiàn)程(可以是從Servlet容器中已有的主線(xiàn)程池獲取,也可以另外維護(hù)一個(gè)線(xiàn)程池,不同容器實(shí)現(xiàn)可能不一樣),然后在這個(gè)新的線(xiàn)程中繼續(xù)處理請(qǐng)求,而原先的線(xiàn)程將被回收到主線(xiàn)程池中。事實(shí)上,這種方式對(duì)性能的改進(jìn)不大,因?yàn)槿绻碌木€(xiàn)程和初始線(xiàn)程共享同一個(gè)線(xiàn)程池的話(huà),相當(dāng)于閑置下了一個(gè)線(xiàn)程,但同時(shí)又占用了另一個(gè)線(xiàn)程。
當(dāng)然,除了調(diào)用AsyncContext的start()方法,我們還可以通過(guò)手動(dòng)創(chuàng)建線(xiàn)程的方式來(lái)實(shí)現(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();
}
}
自己手動(dòng)創(chuàng)建新線(xiàn)程一般是不被鼓勵(lì)的,并且此時(shí)線(xiàn)程不能重用。因此,一種更好的辦法是我們自己維護(hù)一個(gè)線(xiàn)程池。這個(gè)線(xiàn)程池不同于Servlet容器的主線(xiàn)程池,如下圖:

在上圖中,用戶(hù)發(fā)起的請(qǐng)求首先交由Servlet容器主線(xiàn)程池中的線(xiàn)程處理,在該線(xiàn)程中,我們獲取到AsyncContext,然后將其交給異步處理線(xiàn)程池??梢酝ㄟ^(guò)Java提供的Executor框架來(lái)創(chuàng)建線(xiàn)程池:
@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對(duì)請(qǐng)求的處理雖然是異步的,但是對(duì)InputStream和OutputStream的IO操作卻依然是阻塞的,對(duì)于數(shù)據(jù)量大的請(qǐng)求體或者返回體,阻塞IO也將導(dǎo)致不必要的等待。因此在Servlet 3.1中引入了非阻塞IO(參考下圖紅框內(nèi)容),通過(guò)在HttpServletRequest和HttpServletResponse中分別添加ReadListener和WriterListener方式,只有在IO數(shù)據(jù)滿(mǎn)足一定條件時(shí)(比如數(shù)據(jù)準(zhǔn)備好時(shí)),才進(jìn)行后續(xù)的操作。

對(duì)應(yīng)的代碼示:
@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添加了一個(gè)ReadListener,并在ReadListener的onAllDataRead()方法中完成了長(zhǎng)時(shí)處理過(guò)程。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Spring?Boot?Admin?添加報(bào)警提醒和登錄驗(yàn)證功能的具體實(shí)現(xiàn)
報(bào)警提醒功能是基于郵箱實(shí)現(xiàn)的,當(dāng)然也可以使用其他的提醒功能,如釘釘或飛書(shū)機(jī)器人提醒也是可以的,但郵箱報(bào)警功能的實(shí)現(xiàn)成本最低,所以本文我們就來(lái)看郵箱的報(bào)警提醒功能的具體實(shí)現(xiàn)2022-01-01
spring實(shí)現(xiàn)動(dòng)態(tài)切換、添加數(shù)據(jù)源及源碼分析
這篇文章主要給大家介紹了關(guān)于spring實(shí)現(xiàn)動(dòng)態(tài)切換、添加數(shù)據(jù)源及源碼分析的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-09-09
IDEA連接MySQL后管理數(shù)據(jù)庫(kù)的操作指南
本節(jié)就來(lái)教大家如何在IDEA連接MySQL后管理數(shù)據(jù)庫(kù)(創(chuàng)建/修改/刪除數(shù)據(jù)庫(kù)、創(chuàng)建/修改/刪除表、插入/更新/刪除/查詢(xún)表記錄),文中通過(guò)圖文結(jié)合的方式給大家講解的非常詳細(xì),需要的朋友可以參考下2024-05-05
SpringBoot排除自動(dòng)加載數(shù)據(jù)源方式
這篇文章主要介紹了SpringBoot排除自動(dòng)加載數(shù)據(jù)源方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-05-05

