千萬不要這樣使用@Async注解
你知道的越多,不知道的就越多,業(yè)余的像一棵小草!
你來,我們一起精進!你不來,我和你的競爭對手一起精進!
編輯:業(yè)余草
chuckfang.com/2019/11/13/Async
推薦:https://www.xttblog.com/?p=5288
在實際的項目中,對于一些用時比較長的代碼片段或者函數(shù),我們可以采用異步的方式來執(zhí)行,這樣就不會影響整體的流程了。比如我在一個用戶請求中需要上傳一些文件,但是上傳文件的耗時會相對來說比較長,這個時候如果上傳文件的成功與否不影響主流程的話,就可以把上傳文件的操作異步化,在spring boot中比較常見的方式就是把要異步執(zhí)行的代碼片段封裝成一個函數(shù),然后在函數(shù)頭使用@Async注解,就可以實現(xiàn)代碼的異步執(zhí)行(當然首先得在啟動類上加上@EnableAsync注解了)。

具體的使用方式這里我也就不再演示了,網(wǎng)上教大家使用@Async的很多。今天我要講的并不是怎么去使用@Async注解,而是講我在實際開發(fā)過程中遇到的一個坑,希望你不要再犯。
首先,再明確一點,學(xué)習(xí)一個知識,第一步是找到相應(yīng)的官網(wǎng)或是比較權(quán)威的網(wǎng)站。
那么這個坑是什么呢?就是如果你在同一個類里面調(diào)用一個自己的被@Async修飾的函數(shù)時,這個函數(shù)將不會被異步執(zhí)行,它依然是同步執(zhí)行的!所以你如果沒有經(jīng)過測試就想當然的以為只要在方法頭加上@Async就能達到異步的效果,那么你很有可能會得到相反的效果。這個是很要命的。
所以我來給你們演示一下,這個效果是多么恐怖。為什么說它恐怖,是因為在程序員的眼中,一切不符合期望的行為都是bug,bug能不恐怖嗎?
首先我們先看一個正確使用的方式,建一個spring boot項目,如果你是用Intellij IDEA新建的項目,記得勾上web的依賴。
項目建好后,我們在啟動類上加上@EnableAsync注解:
import?org.springframework.boot.SpringApplication;
import?org.springframework.boot.autoconfigure.SpringBootApplication;
import?org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync
public?class?AsyncdemoApplication?{
????public?static?void?main(String[]?args)?{
????????SpringApplication.run(AsyncdemoApplication.class,?args);
????}
}
然后再新建一個類Task,用來放三個異步任務(wù)doTaskOne、doTaskTwo、doTaskThree:
import?org.springframework.scheduling.annotation.Async;
import?org.springframework.stereotype.Component;
import?java.util.Random;
/**
?*?@author?https://www.chuckfang.top
?*?@date?Created?on?2019/11/12?11:34
?*/
@Component
public?class?Task?{
????public?static?Random?random?=?new?Random();
????@Async
????public?void?doTaskOne()?throws?Exception?{
????????System.out.println("開始做任務(wù)一");
????????long?start?=?System.currentTimeMillis();
????????Thread.sleep(random.nextInt(10000));
????????long?end?=?System.currentTimeMillis();
????????System.out.println("完成任務(wù)一,耗時:"?+?(end?-?start)?+?"毫秒");
????}
????@Async
????public?void?doTaskTwo()?throws?Exception?{
????????System.out.println("開始做任務(wù)二");
????????long?start?=?System.currentTimeMillis();
????????Thread.sleep(random.nextInt(10000));
????????long?end?=?System.currentTimeMillis();
????????System.out.println("完成任務(wù)二,耗時:"?+?(end?-?start)?+?"毫秒");
????}
????@Async
????public?void?doTaskThree()?throws?Exception?{
????????System.out.println("開始做任務(wù)三");
????????long?start?=?System.currentTimeMillis();
????????Thread.sleep(random.nextInt(10000));
????????long?end?=?System.currentTimeMillis();
????????System.out.println("完成任務(wù)三,耗時:"?+?(end?-?start)?+?"毫秒");
????}
}
在單元測試類上注入Task,在測試用例上測試這三個方法的執(zhí)行過程:
@SpringBootTest
class?AsyncdemoApplicationTests?{
????public?static?Random?random?=?new?Random();
????@Autowired
????Task?task;
????@Test
????void?contextLoads()?throws?Exception?{
????????task.doTaskOne();
????????task.doTaskTwo();
????????task.doTaskThree();
????????Thread.sleep(10000);
????}
}
為了讓這三個方法執(zhí)行完,我們需要再單元測試用例上的最后一行加上一個延時,不然等函數(shù)退出了,異步任務(wù)還沒執(zhí)行完。
我們啟動看看效果:
?開始做任務(wù)三
?
開始做任務(wù)二
開始做任務(wù)一
完成任務(wù)一,耗時:4922毫秒
完成任務(wù)三,耗時:6778毫秒
完成任務(wù)二,耗時:6960毫秒
我們看到三個任務(wù)確實是異步執(zhí)行的,那我們再看看錯誤的使用方法。
我們在測試類里面把這三個函數(shù)再寫一遍,并在測試用例上調(diào)用測試類自己的方法:
@SpringBootTest
class?AsyncdemoApplicationTests?{
????public?static?Random?random?=?new?Random();
????@Test
????void?contextLoads()?throws?Exception?{
????????doTaskOne();
????????doTaskTwo();
????????doTaskThree();
????????Thread.sleep(10000);
????}
????@Async
????public?void?doTaskOne()?throws?Exception?{
????????System.out.println("開始做任務(wù)一");
????????long?start?=?System.currentTimeMillis();
????????Thread.sleep(random.nextInt(10000));
????????long?end?=?System.currentTimeMillis();
????????System.out.println("完成任務(wù)一,耗時:"?+?(end?-?start)?+?"毫秒");
????}
????@Async
????public?void?doTaskTwo()?throws?Exception?{
????????System.out.println("開始做任務(wù)二");
????????long?start?=?System.currentTimeMillis();
????????Thread.sleep(random.nextInt(10000));
????????long?end?=?System.currentTimeMillis();
????????System.out.println("完成任務(wù)二,耗時:"?+?(end?-?start)?+?"毫秒");
????}
????@Async
????public?void?doTaskThree()?throws?Exception?{
????????System.out.println("開始做任務(wù)三");
????????long?start?=?System.currentTimeMillis();
????????Thread.sleep(random.nextInt(10000));
????????long?end?=?System.currentTimeMillis();
????????System.out.println("完成任務(wù)三,耗時:"?+?(end?-?start)?+?"毫秒");
????}
}
我們再看看效果:
?開始做任務(wù)一
?
完成任務(wù)一,耗時:9284毫秒
開始做任務(wù)二
完成任務(wù)二,耗時:8783毫秒
開始做任務(wù)三
完成任務(wù)三,耗時:943毫秒
它們竟然是順序執(zhí)行的!也就是同步執(zhí)行,并沒有達到異步的效果,這要是在生產(chǎn)上使用,豈不涼涼。
這種問題如果不進行測試還是比較難發(fā)現(xiàn)的,特別是你想要異步執(zhí)行的代碼并不會執(zhí)行太久,也就是同步執(zhí)行你也察覺不出來,或者說你根本發(fā)現(xiàn)不了它是不是異步執(zhí)行。這種錯誤也很容易犯,特別是當你把一個類里面的方法提出來想要異步執(zhí)行的時候,你并不會想著新建一個類來放這個方法,而是會在當前類上直接抽取為一個方法,然后在方法頭上加上@Async注解,你以為這樣就完事了,其實并沒有起到異步的作用!我也是在改進我們項目的文件上傳時才發(fā)現(xiàn)這個問題的。因為文件上傳也不會花費太久,所以真的很隱蔽。
其實@Async的這個性質(zhì)在官網(wǎng)上已經(jīng)有過說明了,官網(wǎng):https://www.baeldung.com/spring-async是這樣說的:
?First – let’s go over the rules – @Async has two limitations:
it must be applied to public methods only self-invocation – calling the async method from within the same class – won’t work The reasons are simple – 「the method needs to be *public*」 so that it can be proxied. And 「self-invocation doesn’t work」 because it bypasses the proxy and calls the underlying method directly.
?
文章在一開始就提到了@Async的兩個限制,其中第二個就是調(diào)用自己類上的異步方法是不起作用的。下面也講了原因,就是這種使用方式繞過了代理而直接調(diào)用了方法,所以肯定是同步的了。從這里,我們也知道了另外一個知識點,就是@Async注解其實是通過代理的方式來實現(xiàn)異步調(diào)用的。
上面這個錯誤使用方法,我目前沒有在網(wǎng)上看到過有人說明,甚至在程序員DD的博客中也沒有對此進行說明,我深表遺憾。希望你看完我的博客之后不要再犯同樣的錯誤了,或者你趕快檢查一下你自己的項目中有沒有這樣使用@Async注解的。如果覺得文章不錯,可以推薦給同事看哦,提醒他們正確使用@Async。
