SpringBoot | 第二十一章:非同步開發之非同步呼叫
前言
上一章節,我們知道了如何進行非同步請求的處理。除了非同步請求,一般上我們用的比較多的應該是非同步呼叫。通常在開發過程中,會遇到一個方法是和實際業務無關的,沒有緊密性的。比如記錄日誌資訊等業務。這個時候正常就是啟一個新執行緒去做一些業務處理,讓主執行緒非同步的執行其他業務。所以,本章節重點說下在
SpringBoot
中如何進行非同步呼叫及其相關知識和注意點。
一點知識
何為非同步呼叫
說非同步呼叫
前,我們說說它對應的同步呼叫
。通常開發過程中,一般上我們都是同步呼叫
,即:程式按定義的順序依次執行的過程,每一行程式碼執行過程必須等待上一行程式碼執行完畢後才執行。而非同步呼叫
指:程式在執行時,無需等待執行的返回值可繼續執行後面的程式碼。顯而易見,同步有依賴相關性,而非同步沒有,所以非同步可併發
題外話:處理非同步
、同步
外,還有一個叫回撥
。其主要是解決非同步方法執行結果的處理方法,比如在希望非同步呼叫結束時返回執行結果,這個時候就可以考慮使用回撥機制。
Async非同步呼叫
在
SpringBoot
中使用非同步呼叫是很簡單的,只需要使用@Async
註解即可實現方法的非同步呼叫。
注意:需要在啟動類加入@EnableAsync
使非同步呼叫@Async
註解生效。
@SpringBootApplication @EnableAsync @Slf4j public class Chapter21Application { public static void main(String[] args) { SpringApplication.run(Chapter21Application.class, args); log.info("Chapter21啟動!"); } }
@Async非同步呼叫
使用@Async
很簡單,只需要在需要非同步執行的方法上加入此註解即可。這裡建立一個控制層和一個服務層,進行簡單示例下。
SyncService.java
@Component public class SyncService { @Async public void asyncEvent() throws InterruptedException { //休眠1s Thread.sleep(1000); //log.info("非同步方法輸出:{}!", System.currentTimeMillis()); } public void syncEvent() throws InterruptedException { Thread.sleep(1000); //log.info("同步方法輸出:{}!", System.currentTimeMillis()); } }
控制層:AsyncController.java
@RestController @Slf4j public class AsyncController { @Autowired SyncService syncService; @GetMapping("/async") public String doAsync() throws InterruptedException { long start = System.currentTimeMillis(); log.info("方法執行開始:{}", start); //呼叫同步方法 syncService.syncEvent(); long syncTime = System.currentTimeMillis(); log.info("同步方法用時:{}", syncTime - start); //呼叫非同步方法 syncService.asyncEvent(); long asyncTime = System.currentTimeMillis(); log.info("非同步方法用時:{}", asyncTime - syncTime); log.info("方法執行完成:{}!",asyncTime); return "async!!!"; } }
應用啟動後,可以看見控制檯輸出:
2018-08-16 22:21:35.949 INFO 17152 --- [nio-8080-exec-5] c.l.l.s.c.controller.AsyncController : 方法執行開始:1534429295949 2018-08-16 22:21:36.950 INFO 17152 --- [nio-8080-exec-5] c.l.l.s.c.controller.AsyncController : 同步方法用時:1001 2018-08-16 22:21:36.950 INFO 17152 --- [nio-8080-exec-5] c.l.l.s.c.controller.AsyncController : 非同步方法用時:0 2018-08-16 22:21:36.950 INFO 17152 --- [nio-8080-exec-5] c.l.l.s.c.controller.AsyncController : 方法執行完成:1534429296950! 2018-08-16 22:21:37.950 INFO 17152 --- [cTaskExecutor-3] c.l.l.s.chapter21.service.SyncService : 非同步方法內部執行緒名稱:SimpleAsyncTaskExecutor-3!
可以看出,呼叫非同步方法時,是立即返回的,基本沒有耗時。
這裡有幾點需要注意下:
- 在預設情況下,未設定
TaskExecutor
時,預設是使用SimpleAsyncTaskExecutor
這個執行緒池,但此執行緒不是真正意義上的執行緒池,因為執行緒不重用,每次呼叫都會建立一個新的執行緒。可通過控制檯日誌輸出可以看出,每次輸出執行緒名都是遞增的。 - 呼叫的非同步方法,不能為
同一個類
的方法,簡單來說,因為Spring
在啟動掃描時會為其建立一個代理類,而同類呼叫時,還是呼叫本身的代理類的,所以和平常呼叫是一樣的。其他的註解如@Cache
等也是一樣的道理,說白了,就是Spring
的代理機制造成的。
自定義執行緒池
前面有提到,在預設情況下,系統使用的是預設的
SimpleAsyncTaskExecutor
進行執行緒建立。所以一般上我們會自定義執行緒池來進行執行緒的複用。
建立一個自定義的ThreadPoolTaskExecutor
執行緒池:
Config.java
@Configuration public class Config { /** * 配置執行緒池 * @return */ @Bean(name = "asyncPoolTaskExecutor") public ThreadPoolTaskExecutor getAsyncThreadPoolTaskExecutor() { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); taskExecutor.setCorePoolSize(20); taskExecutor.setMaxPoolSize(200); taskExecutor.setQueueCapacity(25); taskExecutor.setKeepAliveSeconds(200); taskExecutor.setThreadNamePrefix("oKong-"); // 執行緒池對拒絕任務(無執行緒可用)的處理策略,目前只支援AbortPolicy、CallerRunsPolicy;預設為後者 taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); taskExecutor.initialize(); return taskExecutor; } }
此時,使用的是就只需要在@Async
加入執行緒池名稱即可:
@Async("asyncPoolTaskExecutor") public void asyncEvent() throws InterruptedException { //休眠1s Thread.sleep(1000); log.info("非同步方法內部執行緒名稱:{}!", Thread.currentThread().getName()); }
再次啟動應用,就可以看見已經是使用自定義的執行緒了。
2018-08-16 22:32:02.676 INFO 4516 --- [nio-8080-exec-1] c.l.l.s.c.controller.AsyncController : 方法執行開始:1534429922676 2018-08-16 22:32:03.681 INFO 4516 --- [nio-8080-exec-1] c.l.l.s.c.controller.AsyncController : 同步方法用時:1005 2018-08-16 22:32:03.693 INFO 4516 --- [nio-8080-exec-1] c.l.l.s.c.controller.AsyncController : 非同步方法用時:12 2018-08-16 22:32:03.693 INFO 4516 --- [nio-8080-exec-1] c.l.l.s.c.controller.AsyncController : 方法執行完成:1534429923693! 2018-08-16 22:32:04.694 INFO 4516 --- [ oKong-1] c.l.l.s.chapter21.service.SyncService : 非同步方法內部執行緒名稱:oKong-1!
這裡簡單說明下,關於ThreadPoolTaskExecutor
引數說明:
- corePoolSize:執行緒池維護執行緒的最少數量
- keepAliveSeconds:允許的空閒時間,當超過了核心執行緒出之外的執行緒在空閒時間到達之後會被銷燬
- maxPoolSize:執行緒池維護執行緒的最大數量,只有在緩衝佇列滿了之後才會申請超過核心執行緒數的執行緒
- queueCapacity:快取佇列
- rejectedExecutionHandler:執行緒池對拒絕任務(無執行緒可用)的處理策略。這裡採用了
CallerRunsPolicy
策略,當執行緒池沒有處理能力的時候,該策略會直接在 execute 方法的呼叫執行緒中執行被拒絕的任務;如果執行程式已關閉,則會丟棄該任務。還有一個是AbortPolicy
策略:處理程式遭到拒絕將丟擲執行時RejectedExecutionException
。
而在一些場景下,若需要在關閉執行緒池時等待當前排程任務完成後才開始關閉,可以通過簡單的配置,進行優雅的停機
策略配置。關鍵就是通過setWaitForTasksToCompleteOnShutdown(true)
和setAwaitTerminationSeconds
方法。
- setWaitForTasksToCompleteOnShutdown:表明等待所有執行緒執行完,預設為
false
。 - setAwaitTerminationSeconds:等待的時間,因為不能無限的等待下去。
所以,執行緒池完整配置為:
@Bean(name = "asyncPoolTaskExecutor") public ThreadPoolTaskExecutor getAsyncThreadPoolTaskExecutor() { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); taskExecutor.setCorePoolSize(20); taskExecutor.setMaxPoolSize(200); taskExecutor.setQueueCapacity(25); taskExecutor.setKeepAliveSeconds(200); taskExecutor.setThreadNamePrefix("oKong-"); // 執行緒池對拒絕任務(無執行緒可用)的處理策略,目前只支援AbortPolicy、CallerRunsPolicy;預設為後者 taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); //排程器shutdown被呼叫時等待當前被排程的任務完成 taskExecutor.setWaitForTasksToCompleteOnShutdown(true); //等待時長 taskExecutor.setAwaitTerminationSeconds(60); taskExecutor.initialize(); return taskExecutor; }
非同步回撥及超時處理
對於一些業務場景下,需要非同步回撥的返回值時,就需要使用非同步回撥來完成了。主要就是通過
Future
進行非同步回撥。
非同步回撥
修改下非同步方法的返回型別,加入Future
。
@Async("asyncPoolTaskExecutor") public Future<String> asyncEvent() throws InterruptedException { //休眠1s Thread.sleep(1000); log.info("非同步方法內部執行緒名稱:{}!", Thread.currentThread().getName()); return new AsyncResult<>("非同步方法返回值"); }
其中AsyncResult
是Spring
提供的一個Future
介面的子類。
然後通過isDone
方法,判斷是否已經執行完畢。
@GetMapping("/async") public String doAsync() throws InterruptedException { long start = System.currentTimeMillis(); log.info("方法執行開始:{}", start); //呼叫同步方法 syncService.syncEvent(); long syncTime = System.currentTimeMillis(); log.info("同步方法用時:{}", syncTime - start); //呼叫非同步方法 Future<String> doFutrue = syncService.asyncEvent(); while(true) { //判斷非同步任務是否完成 if(doFutrue.isDone()) { break; } Thread.sleep(100); } long asyncTime = System.currentTimeMillis(); log.info("非同步方法用時:{}", asyncTime - syncTime); log.info("方法執行完成:{}!",asyncTime); return "async!!!"; }
此時,控制檯輸出:
2018-08-16 23:10:57.021 INFO 9072 --- [nio-8080-exec-1] c.l.l.s.c.controller.AsyncController : 方法執行開始:1534431237020 2018-08-16 23:10:58.025 INFO 9072 --- [nio-8080-exec-1] c.l.l.s.c.controller.AsyncController : 同步方法用時:1005 2018-08-16 23:10:59.037 INFO 9072 --- [ oKong-1] c.l.l.s.chapter21.service.SyncService : 非同步方法內部執行緒名稱:oKong-1! 2018-08-16 23:10:59.040 INFO 9072 --- [nio-8080-exec-1] c.l.l.s.c.controller.AsyncController : 非同步方法用時:1015 2018-08-16 23:10:59.040 INFO 9072 --- [nio-8080-exec-1] c.l.l.s.c.controller.AsyncController : 方法執行完成:1534431239040!
所以,當某個業務功能可以同時拆開一起執行時,可利用非同步回撥機制,可有效的減少程式執行時間,提高效率。
超時處理
對於一些需要非同步回撥的函式,不能無期限的等待下去,所以一般上需要設定超時時間,超時後可將執行緒釋放,而不至於一直堵塞而佔用資源。
對於Future
配置超時,很簡單,通過get
方法即可,具體如下:
//get方法會一直堵塞,直到等待執行完成才返回 //get(long timeout, TimeUnit unit) 在設定時間類未返回結果,會直接排除異常TimeoutException,messages為null String result = doFutrue.get(60, TimeUnit.SECONDS);//60s
超時後,會丟擲異常TimeoutException
類,此時可進行統一異常捕獲即可。
參考資料
總結
本章節主要是講解了
非同步請求
的使用及相關配置,如超時,異常等處理。在剝離一些和業務無關的操作時,就可以考慮使用非同步呼叫
進行其他無關業務操作,以此提供業務的處理效率。或者一些業務場景下可拆分出多個方法進行同步執行又互不影響時,也可以考慮使用非同步呼叫
方式提供執行效率。既然已經講解了非同步相關知識,下一章節就來介紹下定時任務
的使用。
最後
目前網際網路上很多大佬都有
SpringBoot
系列教程,如有雷同,請多多包涵了。本文是作者在電腦前一字一句敲的,每一步都是自己實踐的。若文中有所錯誤之處,還望提出,謝謝。