SpringBoot異步註解@Async解析

        在寫一個綁定設備的接口,需要立即響應。但是有一個增加成長值的需求需要在這個綁定設備的接口中實現,該需求需要http調用其他項目的接口,比較耗時,同時這個需求不需要立即返回。因此,想到使用異步的方式實現該方法。於是開始研究@Async的使用,一開始就進了一個坑。

   實現異步:

  • 在啓動類上添加@EnableAsync註解。
  • 在方法或類上添加@Async註解,同時在異步方法所在的類上添加@Component或@service 等註解,之後通過@Autowired使用異步類。

       1.坑一:將異步方法與調用它的方法寫在了同一個類中

@Component
public class AsyncDemo {

    public void test() {
        //異步方法
        for (int i = 0; i < 10; i++) {
            print();
        }
        //打印當前線程
        for (int i = 0; i < 10; i++) {
            System.out.println("---main: thread name: " + Thread.currentThread().getName()  );
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    @Async
    public void print() {
        System.out.println("---  async: thread name: " + Thread.currentThread().getName()  );
    }
}

測試類:

@RunWith(SpringRunner.class)
@SpringBootTest
public class AsyncTest {

    @Autowired
    private AsyncDemo asyncDemo;

    @Test
    public void test(){
        asyncDemo.test();
    }
 }

結果輸出:

---  async: thread name: main
---  async: thread name: main
---  async: thread name: main
---  async: thread name: main
---  async: thread name: main
---  async: thread name: main
---  async: thread name: main
---  async: thread name: main
---  async: thread name: main
---  async: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main

發現異步方法變成了同步執行。於是查找源碼:@EnableAsync註解有個默認模式是AdviceMode.PROXY。該模式會走

具體代碼這裏不在追述,詳細請看:異步任務spring @Async註解源碼解析

簡述一下:spring 在掃描bean的時候會掃描方法上是否包含@Async註解,如果包含,spring會爲這個bean動態地生成一個子類(即代理類,proxy),代理類是繼承原來那個bean的。此時,當這個有註解的方法被調用的時候,實際上是由代理類來調用的,代理類在調用時增加異步作用。然而,如果這個有註解的方法是被同一個類中的其他方法調用的,那麼該方法的調用並沒有通過代理類,而是直接通過原來的那個bean,所以就沒有增加異步作用,我們看到的現象就是@Async註解無效。

僞代碼:

@Service  
class A{  
    @Async
    method b(){...}  
 
    method a(){    //標記1  
        b();  
    }  
}  
 
//Spring掃描註解後,創建了另外一個代理類,併爲有註解的方法加上異步效果  
class proxy$A{  
    A objectA = new A();  
    method b(){    //標記2  
        //異步執行Async  
        objectA.b();  
    }  
 
    method a(){    //標記3  
        objectA.a();    //由於a()沒有註解,所以不會異步執行,而是直接調用A的實例的a()方法  
    }  
}

當我們調用A的bean的a()方法的時候,也是被proxyA攔截,執行proxyA攔截,執行proxyA.a()(標記3),然而,由以上代碼可知,這時候它調用的是objectA.a(),也就是由原來的bean來調用a()方法了,所以代碼跑到了“標記1”。由此可見,“標記2”並沒有被執行到,所以異步執行的效果也沒有運行。

以上解釋:轉自https://blog.csdn.net/clementad/article/details/47339519 

          參考:https://stackoverflow.com/questions/18590170/transactional-does-not-work-on-method-level

查看源碼:

啓動類的註解@EnableAsync中一段代碼:

	/**
	 * Indicate how async advice should be applied.
	 * <p><b>The default is {@link AdviceMode#PROXY}.</b>
	 * Please note that proxy mode allows for interception of calls through the proxy
	 * only. Local calls within the same class cannot get intercepted that way; an
	 * {@link Async} annotation on such a method within a local call will be ignored
	 * since Spring's interceptor does not even kick in for such a runtime scenario.
	 * For a more advanced mode of interception, consider switching this to
	 * {@link AdviceMode#ASPECTJ}.
	 */
	AdviceMode mode() default AdviceMode.PROXY;

即:#model屬性控制應用程序的通知方式,模型默認是AdviceMode PROXY,由其他屬性控制代理的行爲。請注意,代理模式僅允許攔截通過代理進行的調用,並且不能以這種方式攔截同一類中的本地調用。

如果不通過@Autowired方式調用異步方法,而是通過new一個異步方法所在的類的方式調用,@Async也會失效,因爲這也是本地調用。

例如:

異步方法所在的類:

@Component
public class Print {
    @Async
    public void print(){
        System.out.println("---  async: thread name: " + Thread.currentThread().getName());
    }
}

調用:

@Component
public class AsyncDemo {

    public void test() {
        //異步方法
        Print print = new Print();
        for (int i = 0; i < 10; i++) {
            print.print();
        }
        //打印當前線程
        for (int i = 0; i < 10; i++) {
            System.out.println("---main: thread name: " + Thread.currentThread().getName()  );
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

結果:

---  async: thread name: main
---  async: thread name: main
---  async: thread name: main
---  async: thread name: main
---  async: thread name: main
---  async: thread name: main
---  async: thread name: main
---  async: thread name: main
---  async: thread name: main
---  async: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main

修正:

    將異步方法寫在另一個類中。

@Component
public class AsyncDemo {

       @Autowired
    private Print print;

    public void test() {
        //異步方法
        for (int i = 0; i < 10; i++) {
            print.print();
        }
        //打印當前線程
        for (int i = 0; i < 10; i++) {
            System.out.println("---main: thread name: " + Thread.currentThread().getName()  );
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

 修正結果:

---main: thread name: main
---  async: thread name: SimpleAsyncTaskExecutor-3
---  async: thread name: SimpleAsyncTaskExecutor-4
---  async: thread name: SimpleAsyncTaskExecutor-5
---  async: thread name: SimpleAsyncTaskExecutor-7
---  async: thread name: SimpleAsyncTaskExecutor-2
---  async: thread name: SimpleAsyncTaskExecutor-8
---  async: thread name: SimpleAsyncTaskExecutor-1
---  async: thread name: SimpleAsyncTaskExecutor-9
---  async: thread name: SimpleAsyncTaskExecutor-10
---  async: thread name: SimpleAsyncTaskExecutor-6
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main
---main: thread name: main

可以看出異步方法起作用了。但這裏有個問題,調用了10次異步方法,發現開了10個線程。如果異步方法被調用很多次,豈不是要創建很大線程,導致OutOfMemoryError:unable to create new native thread,創建線程數量太多,佔用內存過大。

      2.坑二:@Async使用的默認線程池會導致OOM

如果使用@Async不自定義線程池,會使用默認線程池SimpleAsyncTaskExecutor。

@EnableAsync註解註釋說明:

<p>By default, Spring will be searching for an associated thread pool definition:
either a unique {@link org.springframework.core.task.TaskExecutor} bean in the context,
or an {@link java.util.concurrent.Executor} bean named "taskExecutor" otherwise. If
neither of the two is resolvable, a {@link org.springframework.core.task.SimpleAsyncTaskExecutor}
will be used to process async method invocations. Besides, annotated methods having a
{@code void} return type cannot transmit any exception back to the caller. By default,
such uncaught exceptions are only logged.

翻譯一下:默認情況下,spring會先搜索TaskExecutor類型的bean或者名字爲taskExecutor的Executor類型的bean,都不存在使用SimpleAsyncTaskExecutor執行器。

我們來看一下這個SimpleAsyncTaskExecutor類的註解說明:

{@link TaskExecutor} implementation that fires up a new Thread for each task, executing it asynchronously.
 <p>Supports limiting concurrent threads through the "concurrencyLimit"
 bean property. By default, the number of concurrent threads is unlimited.
 
 <p><b>NOTE: This implementation does not reuse threads!</b> Consider a
  thread-pooling TaskExecutor implementation instead, in particular for
 executing a large number of short-lived tasks.

翻譯:異步執行用戶任務的SimpleAsyncTaskExecutor。每次執行客戶提交給它的任務時,它會啓動新的線程,並允許開發者控制併發線程的上限(concurrencyLimit),從而起到一定的資源節流作用。默認時,concurrencyLimit取值爲-1,即不啓用資源節流。

3.如何自定義線程池

使用默認線程池會導致OOM,那麼我們如何自定義線程池呢。

查看官方文檔:

實現:

1.配置文件:在spingboot的properties中配置

spring.task.execution.pool.core-threads = 3
spring.task.execution.pool.max-threads = 5
spring.task.execution.pool.queue-capacity = 100
spring.task.execution.pool.keep-alive = 10

2.寫配置類

@Configuration
public class ThreadsConfig implements AsyncConfigurer {


    @Value("${spring.task.execution.pool.core-threads}")
    private int corePoolSize;

    @Value("${spring.task.execution.pool.max-threads}")
    private int maxPoolSize;

    @Value("${spring.task.execution.pool.queue-capacity}")
    private int queueCapacity;

    @Value("${spring.task.execution.pool.keep-alive}")
    private int keepAliveSeconds;
    
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor threadPool = new ThreadPoolTaskExecutor();
        //設置核心線程數
        threadPool.setCorePoolSize(corePoolSize);
        //設置最大線程數
        threadPool.setMaxPoolSize(maxPoolSize);
        //線程池所使用的緩衝隊列
        threadPool.setQueueCapacity(queueCapacity);
        //等待任務在關機時完成--表明等待所有線程執行完
        threadPool.setWaitForTasksToCompleteOnShutdown(true);
        // 線程池的工作線程空閒後(指大於核心又小於max的那部分線程),保持存活的時間
        threadPool.setAwaitTerminationSeconds(keepAliveSeconds);
        // 飽和策略默認是直接拋棄任務
        // 初始化線程
        threadPool.initialize();
        return threadPool;
    }

}

配置完成後@Async註解就會使用該線程池,有需要的話可以在getAsyncExecutor()方法添加@Bean("線程池名字")註解指定線程池名字,最後在@Async("線程池名字")使用。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章