在前邊的文章中,和小夥伴一起認識了非同步執行的好處,以及如何進行非同步開發,對,就是使用@Async註解,在使用非同步註解@Async的過程中也存在一些坑,不過通過正確的開啟方式也可以很好的避免,今天想和大家分享下@Async的原理,開始前先溫習下之前的文章哦,
springboot:使用非同步註解@Async獲取執行結果的坑
springboot:巢狀使用非同步註解@Async還會非同步執行嗎
一、引言
在前邊說到在使用@Async的時候,在一個類中兩個@Async的方法巢狀使用會導致非同步失敗,下面把場景重現下,
AsyncContoller.java
package com.example.myDemo.controller; import com.example.myDemo.service.AsyncService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ResponseBody; import java.util.concurrent.ExecutionException; @Controller public class AsyncController { @Autowired private AsyncService asyncService; @GetMapping("/aysnc") @ResponseBody public String asyncMethod(){ try { Long start=System.currentTimeMillis(); //呼叫method3方法,該方法中巢狀了一個非同步方法 String str3=asyncService.method3().get(); Long end=System.currentTimeMillis(); System.out.println("執行時長:"+(end-start)); } catch (ExecutionException | InterruptedException e) { e.printStackTrace(); } return "hello @Async"; } }
下面是method3方法
package com.example.myDemo.service; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.AsyncResult; import org.springframework.stereotype.Service; import java.util.concurrent.Future; @Service @Async public class AsyncService { /** * 第一個非同步方法,睡眠10s返回字串 * * @return */ public Future<String> method() { try { Thread.sleep(10 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } return new AsyncResult("I am method"); } /** * 第三個非同步方法,在該非同步方法中呼叫了另外一個非同步方法 * @return */ public Future<String> method3(){ try{
//睡眠10s Thread.sleep(10*1000); System.out.println(this);
//method方法也是睡眠10s this.method(); }catch (InterruptedException e) { e.printStackTrace(); } return new AsyncResult<>("two async method"); } }
上面便是method3方法,以及巢狀在method3方法中的method方法,這兩個方法體上均沒有標註@Async,只是在這個類上使用了@Async註解,那麼該類中的所有方法都是非同步的。
執行結果如下,
2022-04-30 15:29:47.711 INFO 16836 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 4 ms
com.example.myDemo.service.AsyncService@7e316231
執行時長:20028
從上面可以看到整個方法的執行時長是20多秒,那麼就說明這種同一個類中的巢狀呼叫,@Async是失效的。
二、解決方式
1、把巢狀方法抽到另一個類中
這種方式就是把巢狀的非同步方法method抽取到另外一個類中,下面我們來看下,
OtherService.java
package com.example.myDemo.service; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.AsyncResult; import org.springframework.stereotype.Service; import java.util.concurrent.Future; @Service @Async public class OtherAsyncService { public Future<String> method() { try { Thread.sleep(10 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } return new AsyncResult("I am method"); } }
那麼AsyncService.java則變成下面的樣子
package com.example.myDemo.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.AsyncResult; import org.springframework.stereotype.Service; import java.util.concurrent.Future; @Service @Async public class AsyncService { //注入OtherService @Autowired private OtherAsyncService otherAsyncService; /** * 第三個非同步方法,在該非同步方法中呼叫了另外一個非同步方法 * @return */ public Future<String> method3(){ try{ Thread.sleep(10*1000); System.out.println(this); //呼叫OtherAsyncService的method方法 otherAsyncService.method(); }catch (InterruptedException e) { e.printStackTrace(); } return new AsyncResult<>("two async method"); } }
下面看執行的結果,
2022-04-30 15:44:18.914 INFO 16768 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 5 ms
com.example.myDemo.service.AsyncService@689927ef
執行時長:10016
執行時長10s多點,符合預期。
2、自己注入自己
這種方式很有意思,我斗膽給它取名為“自己注入自己”,在AsyncService類中注入一個AsyncService的例項,如下
package com.example.myDemo.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.AsyncResult; import org.springframework.stereotype.Service; import java.util.concurrent.Future; @Service @Async public class AsyncService { //這裡注入的是AsyncService的例項
@Lazy @Autowired private AsyncService otherAsyncService; /** * 第一個非同步方法,睡眠10s返回字串 * * @return */ public Future<String> method() { try { Thread.sleep(10 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } return new AsyncResult("I am method"); } /** * 第三個非同步方法,在該非同步方法中呼叫了另外一個非同步方法 * @return */ public Future<String> method3(){ try{ Thread.sleep(10*1000); System.out.println(this); otherAsyncService.method(); }catch (InterruptedException e) { e.printStackTrace(); } return new AsyncResult<>("two async method"); } }
小夥伴們注意,我是在AsyncService類中又注入了一個AsyncService的例項,在method3方法中呼叫的是AsyncSerevice的方法method,要區別於下面的呼叫方式
this.method();
下面看下執行結果,
2022-04-30 15:55:30.635 INFO 9788 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 5 ms
com.example.myDemo.service.AsyncService@2ac186f8
執行時長:10015
好了,我們看到執行時長為10s多點,也就是說非同步是生效的,在這種方式中要注意注入的物件必須新增@Lazy註解,否則啟動會報錯哦。
三、原理揭祕
上面已經把巢狀使用的誤區和解決方式已經總結完了,下面到了要揭開@Async面紗的時候了,最好的方式是debug,看下面@Async的debug的過程
可以看到在AsyncController中asyncService是一個代理物件,且使用的方式是cglib,那麼也就是會把其中的方法進行代理,類似下面的程式碼
before();
method3();
after();
也就是對method3進行了代理,這裡的代理指的是把mthod3方法封裝成一個task,交給執行緒池去執行,那麼在method3中的this.method()這句呼叫,也就是普通呼叫了,是同步的,為什麼這樣說,因為這裡的this代表的是AsyncService這個例項物件,
但是如果換成"自己注入自己的方式",例如下圖,
可以看到還是一個AsyncService的cglib代理物件,所以完美解決了巢狀呼叫的問題。
四、總結
本文分析了@Async註解的實現原理及如何使用正確使用巢狀呼叫,
1、@Async註解底層使用的是代理,標記為@Async所在的類在實際呼叫時是一個代理類;
2、合理使用@Async方法的巢狀,可以把巢狀方法抽到另外一個類中;
3、如果在本類中使用巢狀方法,那麼需要自己注入自己,切記加上@Lazy註解;
推薦閱讀
springboot:使用非同步註解@Async獲取執行結果的坑
springboot:巢狀使用非同步註解@Async還會非同步執行嗎