引言
說到異步大家肯定首先會先想到同步。我們先來看看什么是同步?
所謂同步,就是發出一個功能調用時,在沒有得到結果之前,該調用就不返回或繼續執行后續操作。
簡單來說,同步就是必須一件一件事做,等前一件做完了才能做下一件事。
異步:異步就相反,調用在發出之后,這個調用就直接返回了,不需要等結果。
瀏覽器同步
瀏覽器發起一個request然后會一直待一個響應response,在這期間里面它是阻塞的。比如早期我們在我們在逛電商平台的時候買東西我們打開一個商品的頁面,大致流程是不是可能是這樣,每次打開一個頁面都是由一個線程從頭到尾來處理,這個請求需要進行數據庫的訪問需要把商品價格庫存啥的返回頁面,還需要去調用第三方接口,比如優惠券接口等我們只有等到這些都處理完成后這個線程才會把結果響應給瀏覽器,在這等結果期間這個線程只能一直在干等着啥事情也不能干。這樣的話是不是會有有一定的性能問題。大致的流程如下:
瀏覽器異步
為了解決上面同步阻塞的問題,再Servlet3.0發布后,提供了一個新特性:異步處理請求。比如我們還是進入商品詳情頁面,這時候這個前端發起一個請求,然后會有一個線程來執行這個請求,這個請求需要去數據庫查詢庫存、調用第三方接口查詢優惠券等。這時候這個線程就不用干等着呢。它的任務到這就完成了,又可以執行下一個任務了。等查詢數據庫和第三方接口查詢優惠券有結果了,這時候會有一個新的線程來把處理結果返回給前端。這樣的話線程的工作量是不超級飽和,需要不停的干活,連休息的機會都不給了。
- 這個異步是純后端的異步,對前端是無感的,異步也並不會帶來響應時間上的優化,原來該執行多久照樣還是需要執行多久。但是我們的請求線程(Tomcat 線程)為異步servlet之后,我們可以立即返回,依賴於業務的任務用業務線程來執行,也就是說,Tomcat的線程可以立即回收,默認情況下,Tomcat的核心線程是10,最大線程數是200,我們能及時回收線程,也就意味着我們能處理更多的請求,能夠增加我們的吞吐量,這也是異步Servlet的主要作用。
下面我們就來看看Spring mvc 的幾種異步方式吧
https://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等都是有好處的。
- 異步編程可以幫我們高效的利用系統資源。
結束
- 由於自己才疏學淺,難免會有紕漏,假如你發現了錯誤的地方,還望留言給我指出來,我會對其加以修正。
- 如果你覺得文章還不錯,你的轉發、分享、贊賞、點贊、留言就是對我最大的鼓勵。
- 感謝您的閱讀,十分歡迎並感謝您的關注。
站在巨人的肩膀上摘蘋果:
https://blog.csdn.net/f641385712/article/details/88692534