一文搞懂 @Async 注解原理
一、先寫個(gè)Demo
我們直接使用 SpringBoot 搭建個(gè) Demo,首先就是啟動(dòng)類,加入 @EnableAsync 注解。
這就是個(gè)別同學(xué)使用 @Async 注解不生效的原因,沒有在啟動(dòng)類中打開異步的開關(guān)。
再寫一個(gè)Service,定義一個(gè)異步方法async。
@Service
public class TestService {
public final Logger log = LoggerFactory.getLogger(getClass());
@Async
public void async(){
log.info("異步線程消息輸出:{}",Thread.currentThread().getName());
}
}
注意:異步方法所在的類需要被 spring 管理。
定義調(diào)用類,上面說了,需要被 spring 管理,所以調(diào)用的時(shí)候也需要使用注入的方式進(jìn)行調(diào)用,如果使用 new 或者本類方法調(diào)用都是不能生效的。
@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
private TestService testService;
@GetMapping("/async")
public Object async() throws Exception {
testService.async();
return "success";
}
}
啟動(dòng),調(diào)用一下看下輸出。
[task-1]c.z.e.encry.service.TestService:異步線程消息輸出:task-1
可以看到,打印該日志的線程已經(jīng)是另一個(gè)線程,且線程名task-1。那么是不是可以猜測一下使用的線程池中線程名稱前綴為task-,這里先提一嘴,后面我們來揭秘。
到了這,我們的 demo 就搭建完成了,下面開始吃源碼吧,源碼之下無秘密,Debug 啟動(dòng)。
先看一下 @Async 工作流程圖幫助理解。
二、@Async 注解原理
第一步,找到 @Async注解。
注解的內(nèi)部很簡單,就一個(gè) value 屬性,這個(gè)屬性我們后面再說,我先分享一下平常我是如何看源碼的。
- 首先我會(huì)看注釋,尤其是注釋中可以點(diǎn)進(jìn)去的類,比如 See Also中的我會(huì)重點(diǎn)關(guān)注。
- 其次是看注解的參數(shù)調(diào)用的地方。
通過上面兩步,發(fā)現(xiàn) value 參數(shù)調(diào)用的類正好與上面標(biāo)注的 AnnotationAsyncExecutionInterceptor 吻合,所以直接跳到代碼調(diào)用的位置。
其實(shí)在這里我們就可以直接斷點(diǎn),然后根據(jù)調(diào)用棧就知道在哪調(diào)用的了。
打個(gè)斷點(diǎn),啟動(dòng)程序,http 調(diào)用異步方法 async。
需要注意,如果你斷點(diǎn)進(jìn)不來,那就重啟,在應(yīng)用程序啟動(dòng)之后的第一次訪問中會(huì)被攔住。(原因后面說,在第五節(jié)自定義線程池)
點(diǎn)擊圖片中紅框起來的位置,發(fā)現(xiàn)跳到的代碼位置正好與 See Also中標(biāo)注的是同一個(gè)方法AsyncExecutionAspectSupport#determineAsyncExecutor ,說明我們沒有找錯(cuò)地方,那么我們就開始在這個(gè)位置,再加入一個(gè)斷點(diǎn),開始我們的 debug 。
AsyncExecutionAspectSupport#determineAsyncExecutor 方法中,其實(shí)就是 value 參數(shù)生效的地方。
- 80 行代碼處,獲取注解 @Async 的 value 值。
- 83 行代碼處,如果有設(shè)置的 value 值,去 spring 容器中獲取對(duì)應(yīng)的執(zhí)行器,對(duì)于我們這就是獲取對(duì)應(yīng)的線程池。
- 85 行代碼處,如果沒有設(shè)置 value 值,就返回默認(rèn)的 defaultExecutor 。
繼續(xù)斷點(diǎn)處往下走,所以 AsyncExecutionAspectSupport#determineAsyncExecutor 方法就是返回執(zhí)行任務(wù)的線程池。
- 如果為空拋出異常(代碼40行)。
- 否則就封裝我們的異步方法 async 為 Callable。
為什么封裝為一個(gè) Callable 可以評(píng)論區(qū)聊一下,看看八股文忘了沒有。
繼續(xù)往下,就到了代碼 56 行的位置,提交給線程池執(zhí)行任務(wù)。
所以到了這,你看明白了嗎?其實(shí) @Async 注解的核心代碼就是 AsyncExecutionInterceptor#invoke() 方法,只要這個(gè)方法主線找到了,邏輯通了,那么@ Async 注解原理還不是手到擒來。
三、底層是不是使用的線程池
回到這個(gè)問題上來,底層是不是使用的線程池相信你已經(jīng)有了答案了吧,在 springboot 中,起碼是使用的線程池。
當(dāng) value 屬性值為空時(shí),spring會(huì)使用 SimpleAsyncTaskExecutor 執(zhí)行任務(wù),而該類都是通過 new Thread() 執(zhí)行任務(wù)的,具體可查看 SimpleAsyncTaskExecutor#doExecute(Runnable task) 。
在 AsyncExecutionInterceptor 類中,重寫了父類的 getDefaultExecutor ,當(dāng)我們沒有指定 value 參數(shù)時(shí),就會(huì)走到該方法,返回一個(gè)applicationTaskExecutor的線程池。
細(xì)心的同學(xué)應(yīng)該看到了吧,此處線程的前綴就是task-,這不就對(duì)應(yīng)我們文章開頭日志輸出的線程名稱了嗎。
四、線程池的配置
那么這個(gè)線程池是在哪里初始化的呢?我們也沒有看到初始化的代碼?。?/p>
下面我分享一個(gè)找配置的方法,現(xiàn)在我們 beanName 已經(jīng)知道了,直接全局搜索一下不就好了。
這個(gè)還算比較順利,全局一查找,就一個(gè),不就是你嗎。代碼位置(TaskExecutionAutoConfiguration#applicationTaskExecutor)
代碼中就一行,執(zhí)行的 build ,所以我們直接進(jìn)入。
先看一下 configure,簡簡單單的一波 set 的操作。
所以知道線程池的配置在哪了嗎,那肯定就是 new ThreadPoolTaskExecutor 這了。
在 configure 處打一個(gè)斷點(diǎn),即可看到全部的配置信息,包括線程名稱前綴的指定都在這了。
五、可以自定義線程池嗎
那么可以自定義線程池嗎,當(dāng)然可以。還記得文章開頭我們提到的 @Async 注解中的 value 屬性嗎,它的值就是指定線程池名稱的。
我們通過一個(gè)代碼示例來看下是如何使用自定義線程池的。
上文中我們自定義的線程池,把線程名的前綴改為了zuiyuThreadPool-,如果生效,日志將會(huì)打印出線程名稱。
需要注意的就兩點(diǎn):
- @Bean 中自定義線程池的注冊bean名稱
- @Async中指定線程池的名稱,保證與第一步的名稱保持一致。
啟動(dòng)程序,debug 開始。還記得剛開始我們查找 @Async 注解中value參數(shù)使用的地方嗎?這個(gè)地方就是獲取我們注解中值的位置。
在 determineAsyncExecutor 方法處,第 80 行 this.getExecutorQualifier(method) 就是獲取注解中值的代碼,此處返回了zuiyuThreadPool。實(shí)現(xiàn)是 AnnotationAsyncExecutionInterceptor#getExecutorQualifier。
然后 this.findQualifiedExecutor(this.beanFactory, qualifier) 這一行代碼中,只做了一件事,就是拿著 value 值去 beanFactory 中查找對(duì)應(yīng)的 bean 對(duì)象返回。
上面是首次調(diào)用的時(shí)候的邏輯,注意看上圖的 78 行,所以當(dāng)你第二次調(diào)用的時(shí)候就不會(huì)走這個(gè)邏輯了。
那么這個(gè) executors 是什么呢?點(diǎn)進(jìn)去看一下。
在此處打個(gè)斷點(diǎn)看一下,executors 其實(shí)就是一個(gè) map,key 就是注解標(biāo)注的方法,value 就是該方法對(duì)應(yīng)的線程池。
這就是為什么上文中只有程序第一次啟動(dòng)的時(shí)候才會(huì)進(jìn)入到獲取注解屬性值的方法。
六、返回值怎么獲取
如果想獲取接口的返回值有什么方法嗎?
在@Async 注釋的地方,返回類型只能是 void 或者java.util.concurrent.Future。
所以我們只需要把異步的方法,改為這兩種形式的返回值即可。
假如我們想返回 String 類型的值,可以這樣做。
如上圖,我們把異步方法改為 CompletableFuture<String> 的形式就可以返回 String 類型的值了,使用Funture.get() 方法就可以讀取到該值。
其實(shí)歸根結(jié)底還是線程池,你還記得 AsyncExecutionInterceptor#invoke 方法嗎,最后一行代碼不就是 submit 提交任務(wù)。
我們把異步的方法封裝為了一個(gè) Callable task,然后提交。
而在 doSubmit 方法中,校驗(yàn)方法返回值類型是不是Future類型,如果不是直接提交任務(wù),返回 null。
所以,知道為什么在異步方法中需要封裝為 Future 了吧,如果不封裝為Future類型,返回為 null,是獲取不到結(jié)果的。
總結(jié)
@Async 注解的工作原理就是文章開頭給出流程圖所示的流程。核心代碼就是AsyncExecutionInterceptor#invoke() ,重點(diǎn)關(guān)注 determineAsyncExecutor() 與doSubmit()即可。
大致流程如下:
- 從緩存 map 中獲取線程池實(shí)例。
- 如果緩存中存在,直接返回。
- 如果緩存中不存在,判斷是否指定 value值。
- 如果指定 value 值,就去 beanFactory 中獲取對(duì)應(yīng)的線程池實(shí)例。
- 如果沒有指定,value 為空,就獲取 taskExecutor的實(shí)例。
- 返回線程池實(shí)例。
在第5步中,springboot 中如果獲取 taskExecutor 實(shí)例時(shí),因?yàn)橐肓说谌降膉ar,獲取到了第三方的線程池,可能會(huì)遇到意想不到的 bug,這個(gè)點(diǎn)是需要注意的。