@Async注解的使用以及注解失效問題的解決
1. @Async作用范圍
@Async
的注解如下,可以看出該注解可以修飾類
和方法
。
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Async { String value() default ""; }
該注解使用要滿足以下基本要求:
- 1)在方法上使用該@Async注解,申明該方法是一個(gè)異步任務(wù);(必須是public的方法,不能是private的方法,否則注解會(huì)失效?。。?/strong>
- 2)在類上面使用該@Async注解,申明該類中的所有方法都是異步任務(wù);
- 3)方法上一旦標(biāo)記了這個(gè)@Async注解,當(dāng)其它線程調(diào)用這個(gè)方法時(shí),就會(huì)開啟一個(gè)新的子線程去異步處理該業(yè)務(wù)邏輯。
- 4)使用此注解的方法的類對象,必須是spring管理下的bean對象 (如被@Service、@Component等修飾的Bean對象)
- 5)要想使用異步任務(wù),需要在主類上開啟異步配置,即配置上@EnableAsync注解
2. 基本使用方法
2.1 開啟異步注解@EnableAsync
在SpringBoot的啟動(dòng)類上開啟異步任務(wù)注解
@SpringBootApplication @EnableAsync public class AsyncDemoApplication { public static void main(String[] args) { SpringApplication.run(AsyncDemoApplication.class, args); } }
2.2 創(chuàng)建Bean對象及異步方法
@Component public class Aservice { @Async public void MethodA() { System.out.println("當(dāng)前線程為:" + Thread.currentThread().getName()); } }
2.3 在Test方法中進(jìn)行測試
@SpringBootTest class AsyncDemoApplicationTests { @Autowired private Aservice aservice; @Test void contextLoads() { System.out.println("當(dāng)前線程名稱:" + Thread.currentThread().getName()); aservice.MethodA(); } }
測試結(jié)果如下,可以看到確實(shí)開啟了一個(gè)異步任務(wù)。
- 當(dāng)前線程名稱:main
- 當(dāng)前線程為:task-1
2.4 隱藏問題:默認(rèn)線程池配置不合適,導(dǎo)致系統(tǒng)奔潰
@Async注解在使用時(shí),如果不指定線程池的名稱,則使用Spring默認(rèn)的線程池,Spring默認(rèn)的線程池為SimpleAsyncTaskExecutor。
該類型線程池的默認(rèn)配置:
- 默認(rèn)核心線程數(shù):8,
- 最大線程數(shù):Integet.MAX_VALUE,
- 隊(duì)列使用LinkedBlockingQueue,
- 容量是:Integet.MAX_VALUE,
- 空閑線程保留時(shí)間:60s,
- 線程池拒絕策略:AbortPolicy。
解決方法1: 修改配置文件,指定線程池參數(shù)
通過修改SpringBoot的配置文件application.yml
來解決上述問題:
spring: task: execution: thread-name-prefix: MyTask pool: max-size: 6 core-size: 3 keep-alive: 30s queue-capacity: 500
解決方法2:編寫配置類
首先在application.yml
文件中自定義一些鍵值對。
mytask: execution: thread-name-prefix: myThread pool: max-size: 6 core-size: 3 keep-alive: 30 queue-capacity: 500
然后編寫一個(gè)集成了AsyncConfig
的配置類
// 如果沒有在啟動(dòng)類上加注解,在異步任務(wù)配置類中加也是可以的 @EnableAsync @Configuration public class AsyncExecutorConfig implements AsyncConfigurer { @Value(value="${mytask.execution.pool.core-size}") private String CORE_SIZE; @Value(value="${mytask.execution.pool.max-size}") private String MAX_SIZE; @Value("${mytask.execution.pool.queue-capacity}") private String QUEUE_SIZE; @Value("${mytask.execution.thread-name-prefix}") private String THREAD_NAME_PREFIX; @Value("${mytask.execution.pool.keep-alive}") private int KEEP_ALIVE; @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(Integer.parseInt(CORE_SIZE)); executor.setMaxPoolSize(Integer.parseInt(MAX_SIZE)); executor.setQueueCapacity(Integer.parseInt(QUEUE_SIZE)); executor.setThreadNamePrefix(THREAD_NAME_PREFIX); executor.setKeepAliveSeconds(KEEP_ALIVE); executor.setRejectedExecutionHandler( (runnable, threadPoolExecutor) -> { try { threadPoolExecutor.getQueue().put(runnable); } catch (InterruptedException e) { System.out.println("Thread pool receives InterruptedException: " + e); } }); executor.initialize(); return executor; } }
這樣在啟動(dòng)上述任務(wù),就會(huì)打印出修改后的線程名稱。
3. 帶返回值和不帶返回值的異步任務(wù)
3.1 不帶返回值的異步任務(wù)。
在AService.java
中新增異步方法:
@Async public void MethodB() { for (int i = 0; i < 5; i++) { // 模擬任務(wù)執(zhí)行需要5秒 System.out.println("線程-" + Thread.currentThread().getName() + "-業(yè)務(wù)" + i + "執(zhí)行中..."); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }
為了方便測試,編寫一個(gè)Controller接口,來測試該方法。
@RestController public class TestController { @Autowired private Aservice aservice; @GetMapping("/test1") public String test1() { System.out.println(Thread.currentThread().getName() + "線程開始..."); long start = System.currentTimeMillis(); aservice.MethodB(); long end = System.currentTimeMillis(); return "一共耗時(shí):" + (end -start) + "毫秒"; } }
在瀏覽器訪問對應(yīng)接口,發(fā)現(xiàn)僅用了幾毫秒的時(shí)間,實(shí)際MethodB的執(zhí)行時(shí)間為5秒,說明異步方法成功。
3.2 帶返回結(jié)果的異步任務(wù)。
編寫一個(gè)帶返回結(jié)果的異步任務(wù)。
@Async public Future<Integer> methodC() { // 模擬業(yè)務(wù) 執(zhí)行需要5秒 System.out.println("當(dāng)前線程為:" + Thread.currentThread().getName()); Integer result = null; for (int i = 0; i < 5; i++) { // 模擬任務(wù)執(zhí)行需要5秒 System.out.println("線程-" + Thread.currentThread().getName() + "-業(yè)務(wù)" + i + "執(zhí)行中..."); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } result = 1; // 5秒后得到處理后的數(shù)據(jù) System.out.println("methodC 執(zhí)行完畢"); return new AsyncResult<>(result); }
在控制層進(jìn)行調(diào)用,為了驗(yàn)證異步的效果,在控制層也加入3秒中的sleep().
@GetMapping("/getResult") public Integer getResult() throws ExecutionException, InterruptedException { System.out.println(Thread.currentThread().getName() + "線程開始..."); long start = System.currentTimeMillis(); Future<Integer> future = aservice.methodC(); Thread.sleep(3000); Integer result = future.get(); long end = System.currentTimeMillis(); System.out.println("一共耗時(shí):" + (end - start) + "毫秒"); return result; }
執(zhí)行結(jié)果如下,可以看出,盡管主線程中加入了3秒的休眠,整個(gè)任務(wù)還是只用了5秒的異步任務(wù)處理時(shí)長,說明任務(wù)是在異步執(zhí)行的。
http-nio-8086-exec-1線程開始...
當(dāng)前線程為:myThread1
線程-myThread1-業(yè)務(wù)0執(zhí)行中...
線程-myThread1-業(yè)務(wù)1執(zhí)行中...
線程-myThread1-業(yè)務(wù)2執(zhí)行中...
線程-myThread1-業(yè)務(wù)3執(zhí)行中...
線程-myThread1-業(yè)務(wù)4執(zhí)行中...
methodC 執(zhí)行完畢
一共耗時(shí):5053毫秒
有些教程上面可能會(huì)直接在開啟異步任務(wù)的時(shí)候就進(jìn)行g(shù)et()了,這種方法雖然開啟了額外的線程,但主方法其實(shí)也堵塞在get()這行代碼了,相當(dāng)于就還是同步方法了。
如下:
@GetMapping("/getResult1") public Integer getResult1() throws ExecutionException, InterruptedException { System.out.println(Thread.currentThread().getName() #43; "線程開始..."); long start = System.currentTimeMillis(); Integer result = aservice.methodC().get(); Thread.sleep(3000); long end = System.currentTimeMillis(); System.out.println("一共耗時(shí):" + (end - start) + "毫秒"); return result; }
通過運(yùn)行結(jié)果可以看出,一共耗時(shí)8秒,如果是異步任務(wù),只需要5秒。
http-nio-8086-exec-1線程開始...
當(dāng)前線程為:myThread1
線程-myThread1-業(yè)務(wù)0執(zhí)行中...
線程-myThread1-業(yè)務(wù)1執(zhí)行中...
線程-myThread1-業(yè)務(wù)2執(zhí)行中...
線程-myThread1-業(yè)務(wù)3執(zhí)行中...
線程-myThread1-業(yè)務(wù)4執(zhí)行中...
methodC 執(zhí)行完畢
一共耗時(shí):8049毫秒
4. 注解失效的可能原因及解決方法
4.1 異步方法修飾符非public
對于異步任務(wù),要使用public
修飾符
@Component public class Aservice { @Async public void MethodA() { System.out.println("當(dāng)前線程為:" + Thread.currentThread().getName()); } }
4.2 未開啟異步配置
需要在SpringBoot啟動(dòng)類上添加@EnableAsync
注解
@SpringBootApplication @EnableAsync//開啟異步線程配置 public class AsyncDemoApplication { public static void main(String[] args) { SpringApplication.run(AsyncDemoApplication.class, args); } }
或者在Aysnc配置類上添加@EnableAsync
注解
// 如果沒有在啟動(dòng)類上加注解,在異步任務(wù)配置類中加也是可以的 @EnableAsync @Configuration public class AsyncExecutorConfig implements AsyncConfigurer { @Value(value="${mytask.execution.pool.core-size}") private String CORE_SIZE; @Value(value="${mytask.execution.pool.max-size}") private String MAX_SIZE; @Value("${mytask.execution.pool.queue-capacity}") private String QUEUE_SIZE; @Value("${mytask.execution.thread-name-prefix}") private String THREAD_NAME_PREFIX; @Value("${mytask.execution.pool.keep-alive}") private int KEEP_ALIVE; @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(Integer.parseInt(CORE_SIZE)); executor.setMaxPoolSize(Integer.parseInt(MAX_SIZE)); executor.setQueueCapacity(Integer.parseInt(QUEUE_SIZE)); executor.setThreadNamePrefix(THREAD_NAME_PREFIX); executor.setKeepAliveSeconds(KEEP_ALIVE); executor.setRejectedExecutionHandler( (runnable, threadPoolExecutor) -> { try { threadPoolExecutor.getQueue().put(runnable); } catch (InterruptedException e) { System.out.println("Thread pool receives InterruptedException: " + e); } }); executor.initialize(); return executor; } }
4.3 同一個(gè)類的普通方法調(diào)用異步方法
如果在一個(gè)類中,方法A被@Async修飾,而方法B沒有被@Async修飾,并且方法B調(diào)用了方法A,那么會(huì)導(dǎo)致@Async修飾的方法A的注解失效。原因是,對于對于加了@Async的方法A是通過SpringAOP機(jī)制生成的代理類執(zhí)行的,方法B是直接調(diào)用這個(gè)類的方法,因此通過B調(diào)用A,會(huì)使得A也被Spring當(dāng)成普通方法直接調(diào)用,從而使得注解失效。
可以通過以下兩種方式來確保@Async注解生效:
方法1: 將方法A的調(diào)用放在另外一個(gè)Bean上,并通過依賴注入的方式使用該Bean。
@Component public class MyClass { private final MyAsyncService myAsyncService; public MyClass(MyAsyncService myAsyncService) { this.myAsyncService = myAsyncService; } @Async public void A() { // 異步操作內(nèi)容 } public void B() { myAsyncService.A(); } } @Service public class MyAsyncService { @Async public void A() { // 異步操作內(nèi)容 } }
在上述示例中,MyClass類中的方法B調(diào)用了MyAsyncService類中的方法A。由于MyClass類和MyAsyncService類是不同的Bean,在MyClass中直接調(diào)用myAsnycService.A()時(shí),會(huì)觸發(fā)異步操作。
方法2. 在同一個(gè)類內(nèi)部使用self-invocation的方式來調(diào)用被@Async修飾的方法。
@Service public class MyService { @Autowired private MyService self; @Async public void A() { // 異步操作內(nèi)容 } public void B() { self.A(); // 使用self-invocation調(diào)用被@Async修飾的方法A() } }
在上述示例中,MyService類內(nèi)部使用@Autowired將自身注入到了self變量中,在B()方法中通過self.A()來調(diào)用被@Async修飾的A()方法。這樣可以繞過Spring代理機(jī)制,保證A()方法能夠以異步方式執(zhí)行。
無論采取哪種方式,都能確保被@Asnyc修飾的方法在調(diào)用時(shí)能夠以異步方式執(zhí)行,而非直接在當(dāng)前線程執(zhí)行
上述代碼,其實(shí)存在一個(gè)問題,即:因?yàn)?code>MyService類中使用了自身的實(shí)例作為依賴。這種情況下,使用@Autowired
注入會(huì)導(dǎo)致循環(huán)依賴。解決這個(gè)問題有幾種方法:
- 使用
@Lazy
注解:將依賴的注入方式改為懶加載模式,即在需要使用時(shí)才進(jìn)行實(shí)例化。您可以將@Autowired
注解改為@Autowired @Lazy
,以解決循環(huán)依賴的問題。
@Service public class MyService { @Autowired @Lazy private MyService self; @Async public void A() { // 異步操作內(nèi)容 } public void B() { self.A(); // 使用self-invocation調(diào)用被@Async修飾的方法A() } }
- 使用構(gòu)造函數(shù)注入:將依賴通過構(gòu)造函數(shù)進(jìn)行注入而不是字段注入。這樣可以避免循環(huán)依賴,因?yàn)樵跇?gòu)造對象時(shí)就能明確傳遞依賴關(guān)系。
@Service public class MyService { private final MyService self; @Autowired public MyService(MyService self) { this.self = self; } @Async public void A() { // 異步操作內(nèi)容 } public void B() { self.A(); // 使用self-invocation調(diào)用被@Async修飾的方法A() } }
至于用哪種方法,可以根據(jù)實(shí)際需求選擇適合你場景的解決方案。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
基于Properties類操作.properties配置文件方法總結(jié)
這篇文章主要介紹了Properties類操作.properties配置文件方法總結(jié),具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09Springboot 整合 Java DL4J 打造文本摘要生成系統(tǒng)
本文介紹了如何使用SpringBoot整合JavaDeeplearning4j構(gòu)建文本摘要生成系統(tǒng),該系統(tǒng)能夠自動(dòng)從長篇文本中提取關(guān)鍵信息,生成簡潔的摘要,幫助用戶快速了解文本的主要內(nèi)容,技術(shù)實(shí)現(xiàn)包括使用LSTM神經(jīng)網(wǎng)絡(luò)進(jìn)行模型構(gòu)建和訓(xùn)練,并通過SpringBoot集成RESTfulAPI接口2024-11-11Quarkus的Spring擴(kuò)展快速改造Spring項(xiàng)目
這篇文章主要為大家介紹了Quarkus的Spring項(xiàng)目擴(kuò)展,帶大家快速改造Spring項(xiàng)目示例演繹,有需要的朋友可以借鑒參考下,希望能夠有所幫助2022-02-02java排查進(jìn)程占用系統(tǒng)內(nèi)存高方法
這篇文章主要為大家介紹了java進(jìn)程占用系統(tǒng)內(nèi)存高排查方法,2023-06-06IntelliJ IDEA(2019)安裝破解及HelloWorld案例(圖文)
這篇文章主要介紹了IntelliJ IDEA(2019)安裝破解及HelloWorld案例(圖文),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10