@Async注解的使用以及注解失效問題的解決
1. @Async作用范圍
@Async的注解如下,可以看出該注解可以修飾類和方法。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Async {
String value() default "";
}該注解使用要滿足以下基本要求:
- 1)在方法上使用該@Async注解,申明該方法是一個異步任務(wù);(必須是public的方法,不能是private的方法,否則注解會失效?。。?/strong>
- 2)在類上面使用該@Async注解,申明該類中的所有方法都是異步任務(wù);
- 3)方法上一旦標(biāo)記了這個@Async注解,當(dāng)其它線程調(diào)用這個方法時,就會開啟一個新的子線程去異步處理該業(yè)務(wù)邏輯。
- 4)使用此注解的方法的類對象,必須是spring管理下的bean對象 (如被@Service、@Component等修飾的Bean對象)
- 5)要想使用異步任務(wù),需要在主類上開啟異步配置,即配置上@EnableAsync注解
2. 基本使用方法
2.1 開啟異步注解@EnableAsync
在SpringBoot的啟動類上開啟異步任務(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í)開啟了一個異步任務(wù)。
- 當(dāng)前線程名稱:main
- 當(dāng)前線程為:task-1
2.4 隱藏問題:默認(rèn)線程池配置不合適,導(dǎo)致系統(tǒng)奔潰
@Async注解在使用時,如果不指定線程池的名稱,則使用Spring默認(rèn)的線程池,Spring默認(rèn)的線程池為SimpleAsyncTaskExecutor。
該類型線程池的默認(rèn)配置:
- 默認(rèn)核心線程數(shù):8,
- 最大線程數(shù):Integet.MAX_VALUE,
- 隊(duì)列使用LinkedBlockingQueue,
- 容量是:Integet.MAX_VALUE,
- 空閑線程保留時間: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然后編寫一個集成了AsyncConfig的配置類
// 如果沒有在啟動類上加注解,在異步任務(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;
}
}這樣在啟動上述任務(wù),就會打印出修改后的線程名稱。
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);
}
}
}為了方便測試,編寫一個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 "一共耗時:" + (end -start) + "毫秒";
}
}在瀏覽器訪問對應(yīng)接口,發(fā)現(xiàn)僅用了幾毫秒的時間,實(shí)際MethodB的執(zhí)行時間為5秒,說明異步方法成功。

3.2 帶返回結(jié)果的異步任務(wù)。
編寫一個帶返回結(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("一共耗時:" + (end - start) + "毫秒");
return result;
}執(zhí)行結(jié)果如下,可以看出,盡管主線程中加入了3秒的休眠,整個任務(wù)還是只用了5秒的異步任務(wù)處理時長,說明任務(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í)行完畢
一共耗時:5053毫秒
有些教程上面可能會直接在開啟異步任務(wù)的時候就進(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("一共耗時:" + (end - start) + "毫秒");
return result;
}通過運(yùn)行結(jié)果可以看出,一共耗時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í)行完畢
一共耗時: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啟動類上添加@EnableAsync注解
@SpringBootApplication
@EnableAsync//開啟異步線程配置
public class AsyncDemoApplication {
public static void main(String[] args) {
SpringApplication.run(AsyncDemoApplication.class, args);
}
}或者在Aysnc配置類上添加@EnableAsync注解
// 如果沒有在啟動類上加注解,在異步任務(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 同一個類的普通方法調(diào)用異步方法
如果在一個類中,方法A被@Async修飾,而方法B沒有被@Async修飾,并且方法B調(diào)用了方法A,那么會導(dǎo)致@Async修飾的方法A的注解失效。原因是,對于對于加了@Async的方法A是通過SpringAOP機(jī)制生成的代理類執(zhí)行的,方法B是直接調(diào)用這個類的方法,因此通過B調(diào)用A,會使得A也被Spring當(dāng)成普通方法直接調(diào)用,從而使得注解失效。
可以通過以下兩種方式來確保@Async注解生效:
方法1: 將方法A的調(diào)用放在另外一個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()時,會觸發(fā)異步操作。
方法2. 在同一個類內(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)用時能夠以異步方式執(zhí)行,而非直接在當(dāng)前線程執(zhí)行
上述代碼,其實(shí)存在一個問題,即:因?yàn)?code>MyService類中使用了自身的實(shí)例作為依賴。這種情況下,使用@Autowired注入會導(dǎo)致循環(huán)依賴。解決這個問題有幾種方法:
- 使用
@Lazy注解:將依賴的注入方式改為懶加載模式,即在需要使用時才進(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)造對象時就能明確傳遞依賴關(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é)
以上為個人經(jīng)驗(yàn),希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
基于Properties類操作.properties配置文件方法總結(jié)
這篇文章主要介紹了Properties類操作.properties配置文件方法總結(jié),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-09-09
Springboot 整合 Java DL4J 打造文本摘要生成系統(tǒng)
本文介紹了如何使用SpringBoot整合JavaDeeplearning4j構(gòu)建文本摘要生成系統(tǒng),該系統(tǒ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-11
Quarkus的Spring擴(kuò)展快速改造Spring項(xiàng)目
這篇文章主要為大家介紹了Quarkus的Spring項(xiàng)目擴(kuò)展,帶大家快速改造Spring項(xiàng)目示例演繹,有需要的朋友可以借鑒參考下,希望能夠有所幫助2022-02-02
java排查進(jìn)程占用系統(tǒng)內(nèi)存高方法
這篇文章主要為大家介紹了java進(jìn)程占用系統(tǒng)內(nèi)存高排查方法,2023-06-06
IntelliJ IDEA(2019)安裝破解及HelloWorld案例(圖文)
這篇文章主要介紹了IntelliJ IDEA(2019)安裝破解及HelloWorld案例(圖文),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10

