同步代碼寫起來簡單,但就是怕遇到耗時操作,會影響效率和吞吐量。
此時異步代碼才是王者,但涉及多線程和線程池,以及異步結果的獲取,寫起來頗為麻煩。
不過在遇到SpringBoot異步任務時,這個問題就不存在了。因為Spring家族是最替用戶考慮的。
結果就是,像同步一樣簡單,像異步一樣強大。
眾所熟悉的同步代碼
先准備一些代碼,為了模擬耗時操作,在其中加入線程睡眠語句。
同時打印出運行這些代碼的線程信息。如下圖01:
其中一個是沒有返回值的,一個是有返回值的。
然后把它注入到另一個類里進行調用,在調用時也輸出一下主線程信息。如下圖02:
下面是輸出結果,如下圖03:
可以看到這些代碼運行在主線程中,所以這些代碼的耗時操作會影響主線程。
首選的方案就是把耗時操作放入另一個線程中執行(通常稱為工作線程),把主線程解放出來。
同步代碼的異步化改造
由於SpringBoot已經幫我們做好了一切,只需按要求改造即可,只需兩步,真的是非常簡單。
第一步,引入啟用異步任務的注解,@EnableAsync,如下圖04:
第二步,在原來的方法上標上@Async注解,如下圖05:
這就好了,然后像普通方法一樣調用,如下圖06:
看下輸出結果,如下圖07:
可以看到主線程的id是1,而且瞬間執行完。任務在另一個線程id為17的線程中執行,且等耗時操作執行完后才結束。
代碼完全不變,只需加兩個注解,同步立馬變成異步啦。簡直爽歪歪了。
主要是因為這個方法沒有返回值,如果有的話,只需改下返回類型即可。
SpringBoot一共支持三種返回類型,來逐一看下。
第一種,返回類型為Java的Future<?>,如下圖08:
熟悉Java多線程的朋友對這個類都應該不陌生。為了代碼能正常編譯,在方法最后需要return一個這樣的類型。
在同步代碼中,我們原來return的是一個Object類型,顯然不滿足需求,所以SpringBoot就想了一個辦法。
新增了一個類,AsyncResult,使用它進行類型適配,這也是此類的主要作用,保證編譯通過。
這個類就像一個“類型”占位符一樣,如果你真正了解Java多線程的話就會明白,否則絕對不明白。
然后就像普通方法調用一樣調用它,接着通過while循環等待異步任務完成后,輸出返回結果。
注意,我特意輸出了一下方法調用返回的future變量,如下圖09:
輸出結果如下圖10:
可以看到任務是在線程id為17的線程中執行,主線程不斷睡眠等待,直到任務完成后才獲取到任務的返回結果。
重要時刻來臨,可以看到我們輸出的future變量類型是Java的FutureTask類,而我們實際在代碼中return的是Spring的AsyncResult類。
是不是很奇怪呢?其實一點都不怪,這和Java多線程有關,如果還不明白的話,后面有說明。
第二種,返回類型為Spring的ListenableFuture<?>,如下圖11:
可以看到代碼在return的時候寫法是一樣的,那這個類型的好處是什么呢?答案是可以注冊回調。
有了回調,任務在完成后會自動執行回調代碼,所以主線程就不用等了。
因此在調用時要注冊回調代碼,包括成功回調和失敗回調,如下圖12:
注意,我們同樣打印一下方法返回變量listenableFuture的類型。
輸出結果如下圖13:
可以看到此時主線程瞬間執行完畢。任務在線程id為17的線程中執行,完成后執行了回調,且在同一個線程中。
同樣變量listenableFuture的類型是Spring的ListenableFutureTask類,並不是我們在代碼里return的AsyncResult類。
第三種,返回類型為Java的CompletableFuture<?>,如下圖14:
這個類型是Java 8新增的,可以對異步任務進行特殊的操作。
然后進行調用,同樣輸出下返回變量類型,如下圖15:
輸出結果如下圖16:
輸出內容很容易看懂。重點看下返回變量的類型,它就是Java的CompletableFuture<?>類。
那我們在代碼中return的是什么類型呢?如下圖17:
可以看到和真實調用時返回的還是不一樣。如果還不明白,下面來說明下。
Spring在遇到標有@Async的方法時會生成代理,代理做的事情就是把該方法包裝成一個任務submit到線程池中。
在submit的時候會返回真正的返回值,就是上面我們在調用方法時輸出的。
而我們在寫@Async方法代碼時return的是一個類似類型占位符的類,它的一個作用就是保證編譯通過。
另一個作用就是傳遞返回值,在任務執行完成時,把值往外層傳遞。
線程池的個性化按需配置
對於Java來說,幾乎所有的異步執行代碼都是提交到線程池中來執行的,因為線程池可以管理好線程,我們就不用操心了。
不過我們依然可以對線程池進行配置,如核心線程數、最大線程數、內部隊列長度等等。
SpringBoot當然也支持這些配置,按照慣例,這些配置也是放在application.yml配置文件中的。
一些IDE是可以進行自動提示的,如下圖18:
這些配置的前綴是spring.task.execution,主要包括三類配置,線程池中線程的數目和隊列的大小,線程池關閉時的行為,線程名稱的前綴。
有求知欲的朋友可能會尋思,這些配置究竟是如何生效的呢?下面就來滿足一下好奇心,其實很簡單。
SpringBoot的特性之一就是自動配置,這些自動配置代碼都位於這個jar包中,如下圖19:
這個jar包名稱很容易記住,所以最好都能記住,下次有疑問自己就可以去找了。
我們在這個jar包里尋找和任務(task)相關的包名稱,如下圖20:
前兩個類是和任務執行相關的,其中以Properties結尾的類是用於存放application.yml里面的配置的。
以AutoConfiguration結尾的類是用於自動配置的,主要是bean定義的注冊。
這種寫法是SpringBoot自動配置的標准模式,可以看看其它的,都是這樣的。
看下TaskExecutionProperties類,如下圖21:
指定好前綴后,配置文件中的配置項和類中的屬性完全是一一對應的,而且類中屬性可以有默認值,這樣配置文件中沒有配置時就使用默認值。
再來看下TaskExecutionAutoConfiguration類,這里面就注冊了兩個bean,如下圖22:
首先使用剛剛的屬性注冊一個TaskExecutorBuilder類型的bean,然后再使用它注冊一個ThreadPoolTaskExecutor類型的bean。
其實異步任務執行主要是要找到一個線程池的bean,來完成任務的提交,具體尋找邏輯的如下:
1)如果容器中存在唯一一個TaskExecutor類型的bean,那就用它。否則繼續往下。
2)如果容器中存在一個名稱為taskExecutor且類型為Executor的bean,就用它,否則繼續往下。
3)將使用SimpleAsyncTaskExecutor類進行異步方法調用。
void異步方法的異常處理
需要注意的是,返回類型為void的異步方法,將不會向調用者傳遞異常。默認情況下,這些未捕獲的異常僅僅輸出一下日志。
所以對於void方法一定要自己處理好異常。如果恰巧沒處理好,怎么辦呢?不要着急。
SpringBoot提供了統一的未捕獲異常處理方式,只要實現一個接口即可,如下圖23:
我們可以獲取到拋出的異常,還有拋出異常時執行的異步方法,還有調用該異步方法時傳入的參數。
那么,對於有返回值的異步方法,則本身可以傳遞異常,所以不會使用這種方式。這一點需注意。
作者寄語
異步方法的原理很簡單,就是在單獨的線程中執行一個方法或代碼片段。
不過有兩方面需要注意,技術方面和業務方面:
技術方面:
1)如何獲取異步方法的返回值
2)如何處理異步方法產生的異常
3)如何處理異步方法超時的問題
業務方面:
1)異步方法執行成功時對業務的影響
2)異步方法拋出異常時對業務的影響
3)異步方法執行超時時對業務的影響
(END)
>>> 玩轉SpringBoot系列文章 <<<
【玩轉SpringBoot】用好條件相關注解,開啟自動配置之門
【玩轉SpringBoot】看似復雜的Environment其實很簡單
【玩轉SpringBoot】讓錯誤處理重新由web服務器接管
【玩轉SpringBoot】SpringBoot應用的啟動過程一覽表
【玩轉SpringBoot】通過事件機制參與SpringBoot應用的啟動過程
>>> 品Spring系列文章 <<<
品Spring:SpringBoot和Spring到底有沒有本質的不同?
品Spring:SpringBoot輕松取勝bean定義注冊的“第一階段”
品Spring:SpringBoot發起bean定義注冊的“二次攻堅戰”
品Spring:注解之王@Configuration和它的一眾“小弟們”
品Spring:對@PostConstruct和@PreDestroy注解的處理方法
品Spring:對@Autowired和@Value注解的處理方法
品Spring:真沒想到,三十步才能完成一個bean實例的創建
品Spring:關於@Scheduled定時任務的思考與探索,結果尷尬了
>>> 熱門文章集錦 <<<
爸爸又給Spring MVC生了個弟弟叫Spring WebFlux
【面試】吃透了這些Redis知識點,面試官一定覺得你很NB(干貨 | 建議珍藏)
【面試】如果你這樣回答“什么是線程安全”,面試官都會對你刮目相看(建議珍藏)
【面試】迄今為止把同步/異步/阻塞/非阻塞/BIO/NIO/AIO講的這么清楚的好文章(快快珍藏)
【面試】一篇文章幫你徹底搞清楚“I/O多路復用”和“異步I/O”的前世今生(深度好文,建議珍藏)
作者是工作超過10年的碼農,現在任架構師。喜歡研究技術,崇尚簡單快樂。追求以通俗易懂的語言解說技術,希望所有的讀者都能看懂並記住。