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

具體的使用方式這里我也就不再演示了,網(wǎng)上教大家使用@Async的很多。今天我要講的并不是怎么去使用@Async注解,而是講我在實(shí)際開(kāi)發(fā)過(guò)程中遇到的一個(gè)坑,希望你不要再犯。
首先,再明確一點(diǎn),學(xué)習(xí)一個(gè)知識(shí),第一步是找到相應(yīng)的官網(wǎng)或是比較權(quán)威的網(wǎng)站。
那么這個(gè)坑是什么呢?就是如果你在同一個(gè)類(lèi)里面調(diào)用一個(gè)自己的被@Async修飾的函數(shù)時(shí),這個(gè)函數(shù)將不會(huì)被異步執(zhí)行,它依然是同步執(zhí)行的!所以你如果沒(méi)有經(jīng)過(guò)測(cè)試就想當(dāng)然的以為只要在方法頭加上@Async就能達(dá)到異步的效果,那么你很有可能會(huì)得到相反的效果。這個(gè)是很要命的。
所以我來(lái)給你們演示一下,這個(gè)效果是多么恐怖。為什么說(shuō)它恐怖,是因?yàn)樵诔绦騿T的眼中,一切不符合期望的行為都是bug,bug能不恐怖嗎?
首先我們先看一個(gè)正確使用的方式,建一個(gè)spring boot項(xiàng)目,如果你是用Intellij IDEA新建的項(xiàng)目,記得勾上web的依賴(lài)。
項(xiàng)目建好后,我們?cè)趩?dòng)類(lèi)上加上@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);
????}
}
然后再新建一個(gè)類(lèi)Task,用來(lái)放三個(gè)異步任務(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("開(kāi)始做任務(wù)一");
????????long?start?=?System.currentTimeMillis();
????????Thread.sleep(random.nextInt(10000));
????????long?end?=?System.currentTimeMillis();
????????System.out.println("完成任務(wù)一,耗時(shí):"?+?(end?-?start)?+?"毫秒");
????}
????@Async
????public?void?doTaskTwo()?throws?Exception?{
????????System.out.println("開(kāi)始做任務(wù)二");
????????long?start?=?System.currentTimeMillis();
????????Thread.sleep(random.nextInt(10000));
????????long?end?=?System.currentTimeMillis();
????????System.out.println("完成任務(wù)二,耗時(shí):"?+?(end?-?start)?+?"毫秒");
????}
????@Async
????public?void?doTaskThree()?throws?Exception?{
????????System.out.println("開(kāi)始做任務(wù)三");
????????long?start?=?System.currentTimeMillis();
????????Thread.sleep(random.nextInt(10000));
????????long?end?=?System.currentTimeMillis();
????????System.out.println("完成任務(wù)三,耗時(shí):"?+?(end?-?start)?+?"毫秒");
????}
}
在單元測(cè)試類(lèi)上注入Task,在測(cè)試用例上測(cè)試這三個(gè)方法的執(zhí)行過(guò)程:
@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);
????}
}
為了讓這三個(gè)方法執(zhí)行完,我們需要再單元測(cè)試用例上的最后一行加上一個(gè)延時(shí),不然等函數(shù)退出了,異步任務(wù)還沒(méi)執(zhí)行完。
我們啟動(dòng)看看效果:
?開(kāi)始做任務(wù)三
?
開(kāi)始做任務(wù)二
開(kāi)始做任務(wù)一
完成任務(wù)一,耗時(shí):4922毫秒
完成任務(wù)三,耗時(shí):6778毫秒
完成任務(wù)二,耗時(shí):6960毫秒
我們看到三個(gè)任務(wù)確實(shí)是異步執(zhí)行的,那我們?cè)倏纯村e(cuò)誤的使用方法。
我們?cè)跍y(cè)試類(lèi)里面把這三個(gè)函數(shù)再寫(xiě)一遍,并在測(cè)試用例上調(diào)用測(cè)試類(lèi)自己的方法:
@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("開(kāi)始做任務(wù)一");
????????long?start?=?System.currentTimeMillis();
????????Thread.sleep(random.nextInt(10000));
????????long?end?=?System.currentTimeMillis();
????????System.out.println("完成任務(wù)一,耗時(shí):"?+?(end?-?start)?+?"毫秒");
????}
????@Async
????public?void?doTaskTwo()?throws?Exception?{
????????System.out.println("開(kāi)始做任務(wù)二");
????????long?start?=?System.currentTimeMillis();
????????Thread.sleep(random.nextInt(10000));
????????long?end?=?System.currentTimeMillis();
????????System.out.println("完成任務(wù)二,耗時(shí):"?+?(end?-?start)?+?"毫秒");
????}
????@Async
????public?void?doTaskThree()?throws?Exception?{
????????System.out.println("開(kāi)始做任務(wù)三");
????????long?start?=?System.currentTimeMillis();
????????Thread.sleep(random.nextInt(10000));
????????long?end?=?System.currentTimeMillis();
????????System.out.println("完成任務(wù)三,耗時(shí):"?+?(end?-?start)?+?"毫秒");
????}
}
我們?cè)倏纯葱Ч?/p>
?開(kāi)始做任務(wù)一
?
完成任務(wù)一,耗時(shí):9284毫秒
開(kāi)始做任務(wù)二
完成任務(wù)二,耗時(shí):8783毫秒
開(kāi)始做任務(wù)三
完成任務(wù)三,耗時(shí):943毫秒
它們竟然是順序執(zhí)行的!也就是同步執(zhí)行,并沒(méi)有達(dá)到異步的效果,這要是在生產(chǎn)上使用,豈不涼涼。
這種問(wèn)題如果不進(jìn)行測(cè)試還是比較難發(fā)現(xiàn)的,特別是你想要異步執(zhí)行的代碼并不會(huì)執(zhí)行太久,也就是同步執(zhí)行你也察覺(jué)不出來(lái),或者說(shuō)你根本發(fā)現(xiàn)不了它是不是異步執(zhí)行。這種錯(cuò)誤也很容易犯,特別是當(dāng)你把一個(gè)類(lèi)里面的方法提出來(lái)想要異步執(zhí)行的時(shí)候,你并不會(huì)想著新建一個(gè)類(lèi)來(lái)放這個(gè)方法,而是會(huì)在當(dāng)前類(lèi)上直接抽取為一個(gè)方法,然后在方法頭上加上@Async注解,你以為這樣就完事了,其實(shí)并沒(méi)有起到異步的作用!我也是在改進(jìn)我們項(xiàng)目的文件上傳時(shí)才發(fā)現(xiàn)這個(gè)問(wèn)題的。因?yàn)槲募蟼饕膊粫?huì)花費(fèi)太久,所以真的很隱蔽。
其實(shí)@Async的這個(gè)性質(zhì)在官網(wǎng)上已經(jīng)有過(guò)說(shuō)明了,官網(wǎng):https://www.baeldung.com/spring-async是這樣說(shuō)的:
?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.
?
文章在一開(kāi)始就提到了@Async的兩個(gè)限制,其中第二個(gè)就是調(diào)用自己類(lèi)上的異步方法是不起作用的。下面也講了原因,就是這種使用方式繞過(guò)了代理而直接調(diào)用了方法,所以肯定是同步的了。從這里,我們也知道了另外一個(gè)知識(shí)點(diǎn),就是@Async注解其實(shí)是通過(guò)代理的方式來(lái)實(shí)現(xiàn)異步調(diào)用的。
上面這個(gè)錯(cuò)誤使用方法,我目前沒(méi)有在網(wǎng)上看到過(guò)有人說(shuō)明,甚至在程序員DD的博客中也沒(méi)有對(duì)此進(jìn)行說(shuō)明,我深表遺憾。希望你看完我的博客之后不要再犯同樣的錯(cuò)誤了,或者你趕快檢查一下你自己的項(xiàng)目中有沒(méi)有這樣使用@Async注解的。如果覺(jué)得文章不錯(cuò),可以推薦給同事看哦,提醒他們正確使用@Async。
