springboot:非同步註解@Async的前世今生

北漂程式設計師 發表於 2022-04-30
Spring

在前邊的文章中,和小夥伴一起認識了非同步執行的好處,以及如何進行非同步開發,對,就是使用@Async註解,在使用非同步註解@Async的過程中也存在一些坑,不過通過正確的開啟方式也可以很好的避免,今天想和大家分享下@Async的原理,開始前先溫習下之前的文章哦,

springboot:非同步呼叫@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
[email protected]
執行時長: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
[email protected]
執行時長: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
[email protected]
執行時長:10015

好了,我們看到執行時長為10s多點,也就是說非同步是生效的,在這種方式中要注意注入的物件必須新增@Lazy註解,否則啟動會報錯哦。

三、原理揭祕

上面已經把巢狀使用的誤區和解決方式已經總結完了,下面到了要揭開@Async面紗的時候了,最好的方式是debug,看下面@Async的debug的過程

springboot:非同步註解@Async的前世今生

 

可以看到在AsyncController中asyncService是一個代理物件,且使用的方式是cglib,那麼也就是會把其中的方法進行代理,類似下面的程式碼

before();
method3();
after();

也就是對method3進行了代理,這裡的代理指的是把mthod3方法封裝成一個task,交給執行緒池去執行,那麼在method3中的this.method()這句呼叫,也就是普通呼叫了,是同步的,為什麼這樣說,因為這裡的this代表的是AsyncService這個例項物件,

springboot:非同步註解@Async的前世今生

但是如果換成"自己注入自己的方式",例如下圖,

springboot:非同步註解@Async的前世今生

可以看到還是一個AsyncService的cglib代理物件,所以完美解決了巢狀呼叫的問題。

四、總結

本文分析了@Async註解的實現原理及如何使用正確使用巢狀呼叫,

1、@Async註解底層使用的是代理,標記為@Async所在的類在實際呼叫時是一個代理類;

2、合理使用@Async方法的巢狀,可以把巢狀方法抽到另外一個類中;

3、如果在本類中使用巢狀方法,那麼需要自己注入自己,切記加上@Lazy註解;

 

推薦閱讀

springboot:非同步呼叫@Async

springboot:使用非同步註解@Async獲取執行結果的坑

springboot:巢狀使用非同步註解@Async還會非同步執行嗎

springboot:非同步註解@Async的前世今生