天天寫同步,5種SpringMvc非同步請求了解下!
- 2021 年 2 月 3 日
- 筆記
引言
說到非同步大家肯定首先會先想到同步。我們先來看看什麼是同步?
所謂同步,就是發出一個功能調用時,在沒有得到結果之前,該調用就不返回或繼續執行後續操作。
簡單來說,同步就是必須一件一件事做,等前一件做完了才能做下一件事。
非同步:非同步就相反,調用在發出之後,這個調用就直接返回了,不需要等結果。
瀏覽器同步
瀏覽器發起一個request然後會一直待一個響應response,在這期間裡面它是阻塞的。比如早期我們在我們在逛電商平台的時候買東西我們打開一個商品的頁面,大致流程是不是可能是這樣,每次打開一個頁面都是由一個執行緒從頭到尾來處理,這個請求需要進行資料庫的訪問需要把商品價格庫存啥的返回頁面,還需要去調用第三方介面,比如優惠券介面等我們只有等到這些都處理完成後這個執行緒才會把結果響應給瀏覽器,在這等結果期間這個執行緒只能一直在乾等著啥事情也不能幹。這樣的話是不是會有有一定的性能問題。大致的流程如下:
瀏覽器非同步
為了解決上面同步阻塞的問題,再Servlet3.0發布後,提供了一個新特性:非同步處理請求。比如我們還是進入商品詳情頁面,這時候這個前端發起一個請求,然後會有一個執行緒來執行這個請求,這個請求需要去資料庫查詢庫存、調用第三方介面查詢優惠券等。這時候這個執行緒就不用乾等著呢。它的任務到這就完成了,又可以執行下一個任務了。等查詢資料庫和第三方介面查詢優惠券有結果了,這時候會有一個新的執行緒來把處理結果返回給前端。這樣的話執行緒的工作量是不超級飽和,需要不停的幹活,連休息的機會都不給了。
- 這個非同步是純後端的非同步,對前端是無感的,非同步也並不會帶來響應時間上的優化,原來該執行多久照樣還是需要執行多久。但是我們的請求執行緒(Tomcat 執行緒)為非同步servlet之後,我們可以立即返回,依賴於業務的任務用業務執行緒來執行,也就是說,Tomcat的執行緒可以立即回收,默認情況下,Tomcat的核心執行緒是10,最大執行緒數是200,我們能及時回收執行緒,也就意味著我們能處理更多的請求,能夠增加我們的吞吐量,這也是非同步Servlet的主要作用。
下面我們就來看看Spring mvc 的幾種非同步方式吧
//docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-async
在這個之前我們還是先簡單的回顧下Servlet 3.1的非同步: - 客戶端(瀏覽器、app)發送一個請求
- Servlet容器分配一個執行緒來處理容器中的一個servlet
- servlet調用request.startAsync()開啟非同步模式,保存AsyncContext, 然後返回。
- 這個servlet請求執行緒以及所有的過濾器都可以結束,但其響應(response)會等待非同步執行緒處理結束後再返回。
- 其他執行緒使用保存的AsyncContext來完成響應
- 客戶端收到響應
Callable
/** 公眾號:java金融
* 使用Callable
* @return
*/
@GetMapping("callable")
public Callable<String> callable() {
System.out.println(LocalDateTime.now().toString() + "--->主執行緒開始");
Callable<String> callable = () -> {
String result = "return callable";
// 執行業務耗時 5s
Thread.sleep(5000);
System.out.println(LocalDateTime.now().toString() + "--->子任務執行緒("+Thread.currentThread().getName()+")");
return result;
};
System.out.println(LocalDateTime.now().toString() + "--->主執行緒結束");
return callable;
}
public static String doBusiness() {
// 執行業務耗時 10s
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return UUID.randomUUID().toString();
}
- 控制器先返回一個Callable對象
- Spring MVC開始進行非同步處理,並把該Callable對象提交給另一個獨立執行緒的執行器TaskExecutor處理
- DispatcherServlet和所有過濾器都退出Servlet容器執行緒,但此時方法的響應對象仍未返回
- Callable對象最終產生一個返回結果,此時Spring MVC會重新把請求分派回Servlet容器,恢復處理
- DispatcherServlet再次被調用,恢復對Callable非同步處理所返回結果的處理
上面就是Callable的一個執行流程,下面我們來簡單的分析下源碼,看看是怎麼實現的:
我們知道SpringMvc是可以返回json格式數據、或者返回視圖頁面(html、jsp)等,SpringMvc是怎麼實現這個的呢?最主要的一個核心類就是org.springframework.web.method.support.HandlerMethodReturnValueHandler 我們來看看這個類,這個類就是一個介面,總共就兩個方法;
boolean supportsReturnType(MethodParameter returnType);
void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception;
上面這個我們的請求是返回Callable
開啟非同步執行緒的話也就是在handleReturnValue這個方法裡面了,感興趣的大家可以動手去debug下還是比較好調試的。
CompletableFuture 和ListenableFuture
@GetMapping("completableFuture")
public CompletableFuture<String> completableFuture() {
// 執行緒池一般不會放在這裡,會使用static聲明,這只是演示
ExecutorService executor = Executors.newCachedThreadPool();
System.out.println(LocalDateTime.now().toString() + "--->主執行緒開始");
CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(IndexController::doBusiness, executor);
System.out.println(LocalDateTime.now().toString() + "--->主執行緒結束");
return completableFuture;
}
@GetMapping("listenableFuture")
public ListenableFuture<String> listenableFuture() {
// 執行緒池一般不會放在這裡,會使用static聲明,這只是演示
ExecutorService executor = Executors.newCachedThreadPool();
System.out.println(LocalDateTime.now().toString() + "--->主執行緒開始");
ListenableFutureTask<String> listenableFuture = new ListenableFutureTask<>(()-> doBusiness());
executor.execute(listenableFuture);
System.out.println(LocalDateTime.now().toString() + "--->主執行緒結束");
return listenableFuture;
}
註:這種方式記得不要使用內置的不要使用內置的 ForkJoinPool執行緒池,需要自己創建執行緒池否則會有性能問題
WebAsyncTask
@GetMapping("asynctask")
public WebAsyncTask asyncTask() {
SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor();
System.out.println(LocalDateTime.now().toString() + "--->主執行緒開始");
WebAsyncTask<String> task = new WebAsyncTask(1000L, executor, ()-> doBusiness());
task.onCompletion(()->{
System.out.println(LocalDateTime.now().toString() + "--->調用完成");
});
task.onTimeout(()->{
System.out.println("onTimeout");
return "onTimeout";
});
System.out.println(LocalDateTime.now().toString() + "--->主執行緒結束");
return task;
}
DeferredResult
@GetMapping("deferredResult")
public DeferredResult<String> deferredResult() {
System.out.println(LocalDateTime.now().toString() + "--->主執行緒("+Thread.currentThread().getName()+")開始");
DeferredResult<String> deferredResult = new DeferredResult<>();
CompletableFuture.supplyAsync(()-> doBusiness(), Executors.newFixedThreadPool(5)).whenCompleteAsync((result, throwable)->{
if (throwable!=null) {
deferredResult.setErrorResult(throwable.getMessage());
}else {
deferredResult.setResult(result);
}
});
// 非同步請求超時時調用
deferredResult.onTimeout(()->{
System.out.println(LocalDateTime.now().toString() + "--->onTimeout");
});
// 非同步請求完成後調用
deferredResult.onCompletion(()->{
System.out.println(LocalDateTime.now().toString() + "--->onCompletion");
});
System.out.println(LocalDateTime.now().toString() + "--->主執行緒("+Thread.currentThread().getName()+")結束");
return deferredResult;
}
- 上面這幾種非同步方式都是會等到業務doBusiness執行完之後(10s)才會把response給到前端,執行請求的主執行緒會立即結束,響應結果會交給另外的執行緒來返回給前端。
- 這種非同步跟下面的這個所謂的假非同步是不同的,這種情況是由主執行緒執行完成之後立馬返回值(主執行緒)給前端,不會等個5s在返回給前端。
@GetMapping("call")
public String call() {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
return "這是個假非同步";
}
這幾種非同步方式都跟返回Callable 差不多,都有對應的HandlerMethodReturnValueHandler 實現類,無非就是豐富了自己一些特殊的api、比如超時設置啥的,以及執行緒池的創建是誰來創建,執行流程基本都是一樣的。
總結
- 了解spring mvc 的非同步編程,對我們後續學習響應式編程、rxjava、webflux等都是有好處的。
- 非同步編程可以幫我們高效的利用系統資源。
結束
- 由於自己才疏學淺,難免會有紕漏,假如你發現了錯誤的地方,還望留言給我指出來,我會對其加以修正。
- 如果你覺得文章還不錯,你的轉發、分享、讚賞、點贊、留言就是對我最大的鼓勵。
- 感謝您的閱讀,十分歡迎並感謝您的關注。
站在巨人的肩膀上摘蘋果:
//blog.csdn.net/f641385712/article/details/88692534