SpringBoot學習筆記(十七:非同步呼叫)
阿新 • • 發佈:2020-07-15
@[Toc]
“非同步呼叫”對應的是“同步呼叫”, 在實際開發中,有時候為了及時處理請求和進行響應,我們可能使用非同步呼叫,同步呼叫指程式按照定義順序依次執行,每一行程式都必須等待上一行程式執行完成之後才能執行;非同步呼叫指程式在順序執行時,不等待非同步呼叫的語句返回結果就執行後面的程式。非同步呼叫的實現有很多,例如多執行緒、定時任務、訊息佇列等。 這裡學習使用@Async註解來實現非同步呼叫。
# 1、@EnableAsync 首先,我們需要在啟動類上新增 @EnableAsync 註解來宣告開啟非同步方法。 ```java @SpringBootApplication @EnableAsync public class SpringbootAsyncApplication { public static void main(String[] args) { SpringApplication.run(SpringbootAsyncApplication.class, args); } } ```
# 2、@Async 需要注意的,@Async在使用上有一些限制:
* 它只能應用於public修飾的方法
* 自呼叫–從同一個類中呼叫async方法,將不起作用
原因很簡單:
* 只有公共方法,才可以被代理。
* 自呼叫不起作用,因為它越過了代理直接呼叫了方法。
## 2.1、無返回值的非同步方法 這是一個非同步執行的無返回值方法: ```java @Async public void asyncMethodWithVoidReturnType() { System.out.println("非同步無返回值方法 " + Thread.currentThread().getName()); } ```
例項:
* AsyncTask:非同步式任務類,定義了三個非同步式方法。
```java
/**
* @Author 三分惡
* @Date 2020/7/15
* @Description 非同步式任務
*/
@Component
public class AsyncTask {
Logger log= LoggerFactory.getLogger(AsyncTask.class);
private Random random = new Random();
/**
* 定義三個非同步式方法
* @throws InterruptedException
*/
@Async
public void taskOne() throws InterruptedException {
long start = System.currentTimeMillis();
//隨機休眠若干毫秒
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("任務一執行完成耗時{}秒", (end - start)/1000f);
}
@Async
public void taskTwo() throws InterruptedException {
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("任務二執行完成耗時{}秒", (end - start)/1000f);
}
@Async
public void taskThree() throws InterruptedException {
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("任務三執行完成耗時{}秒", (end - start)/1000f);
}
}
```
* 在測試類中呼叫三個非同步式方法:
```java
/**
* @Author 三分惡
* @Date 2020/7/15
* @Description
*/
@SpringBootTest
@RunWith(SpringRunner.class)
public class AsyncTaskTest {
@Autowired
private AsyncTask asyncTask;
Logger log= LoggerFactory.getLogger(AsyncTaskTest.class);
@Test
public void doAsyncTasks(){
try {
long start = System.currentTimeMillis();
//呼叫三個非同步式方法
asyncTask.taskOne();
asyncTask.taskTwo();
asyncTask.taskThree();
Thread.sleep(5000);
long end = System.currentTimeMillis();
log.info("主程式執行完成耗時{}秒", (end - start)/1000f);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
```
執行結果:可以看到三個方法沒有順序執行,這個復執行單元測試,您可能會遇到各種不同的結果,比如:
* 沒有任何任務相關的輸出
* 有部分任務相關的輸出
* 亂序的任務相關的輸出
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200715082638712.png?#pic_center)
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200715085659506.png)
原因是目前doTaskOne、doTaskTwo、doTaskThree三個函式的時候已經是非同步執行了。主程式在非同步呼叫之後,主程式並不會理會這三個函式是否執行完成了,由於沒有其他需要執行的內容,所以程式就自動結束了, 導致了不完整或是沒有輸出任務相關內容的情況。
## 2.1、有返回值的非同步方法 @Async也可以應用有返回值的方法–通過在Future中包裝實際的返回值: ```java /** * 有返回值的非同步方法 * @return */ @Async public Future asyncMethodWithReturnType() {
System.out.println("執行有返回值的非同步方法 "
+ Thread.currentThread().getName());
try {
Thread.sleep(5000);
return new AsyncResult("hello world !!!!");
} catch (InterruptedException e) {
//
}
return null;
}
```
Spring還提供了一個實現Future的AsyncResult類。這個類可用於跟蹤非同步方法執行的結果。
例項: * 我們將2.1的例項改造成有返回值的非同步方法: ```java @Async public Future taskOne() throws InterruptedException {
long start = System.currentTimeMillis();
//隨機休眠若干毫秒
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("任務一執行完成耗時{}秒", (end - start)/1000f);
return new AsyncResult<>("任務一完事了");
}
```
taskTwo、taskThree方法做同樣的改造。
* 測試有返回值的非同步方法:
```java
@Test
public void doFutureTask(){
try {
long start=System.currentTimeMillis();
Future future1=asyncTask.taskOne();
Future future2 = asyncTask.taskTwo();
Future future3 = asyncTask.taskThree();
//三個任務執行完再執行主程式
do {
Thread.sleep(100);
} while (future1.isDone() && future2.isDone() && future3.isDone());
log.info("獲取非同步方法的返回值:{}", future1.get());
Thread.sleep(5000);
long end = System.currentTimeMillis();
log.info("主程式執行完成耗時{}秒", (end - start)/1000f);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
```
執行結果:可以看到三個任務完成後才執行主程式,還輸出了非同步方法的返回值。
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/202007150958486.png?#pic_center)
# 3、 Executor 預設情況下,Spring使用SimpleAsyncTaskExecutor非同步執行這些方法。 可以在兩個級別上重寫預設執行緒池——應用程式級別或方法級別。
## 3.1、方法級別重寫Executor 所需的執行程式需要在配置類中宣告 Executor: ```java /** * @Author 三分惡 * @Date 2020/7/15 * @Description 方法級別重寫執行緒池 */ @Configuration @EnableAsync public class SpringAsyncConfig { @Bean(name = "threadPoolTaskExecutor") public Executor threadPoolTaskExecutor() { return new ThreadPoolTaskExecutor(); } } ``` 然後,在@Async中的屬性提供Executor名稱: ```java @Async("threadPoolTaskExecutor") public void asyncMethodWithConfiguredExecutor() { System.out.println("Execute method with configured executor - " + Thread.currentThread().getName()); } ```
## 3.2、應用級別重寫Executor 配置類應實現AsyncConfigurer介面,重寫getAsyncExecutor()方法。 在這裡,我們將返回整個應用程式的Executor,這樣一來,它就成為執行以@Async註釋的方法的預設Executor: ```java /** * @Author 三分惡 * @Date 2020/7/15 * @Description 應用級別重寫 Excutor */ @Configuration @EnableAsync public class SpringApplicationAsyncConfig implements AsyncConfigurer { @Override public Executor getAsyncExecutor() { return new ThreadPoolTaskExecutor(); } } ```
## 3.3、自定義執行緒池配置 在上面,自定義執行緒池只是簡單地返回了一個執行緒池: ```java return new ThreadPoolTaskExecutor(); ``` 實際上,還可以對執行緒池做一些配置: ```java /** * @Author 三分惡 * @Date 2020/7/15 * @Description */ @Configuration @EnableAsync public class SpringPropertiesAsyncConfig implements AsyncConfigurer { /** * 對執行緒池進行配置 * @return */ @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); taskExecutor.setCorePoolSize(20); taskExecutor.setMaxPoolSize(200); taskExecutor.setQueueCapacity(25); taskExecutor.setKeepAliveSeconds(200); taskExecutor.setThreadNamePrefix("oKong-"); // 執行緒池對拒絕任務(無執行緒可用)的處理策略,目前只支援AbortPolicy、CallerRunsPolicy;預設為後者 taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); taskExecutor.initialize(); return taskExecutor; } } ``` ThreadPoolTaskExecutor配置引數的簡單說明: * corePoolSize:執行緒池維護執行緒的最少數量 * keepAliveSeconds:允許的空閒時間,當超過了核心執行緒出之外的執行緒在空閒時間到達之後會被銷燬 * maxPoolSize:執行緒池維護執行緒的最大數量,只有在緩衝佇列滿了之後才會申請超過核心執行緒數的執行緒 * queueCapacity:快取佇列 * rejectedExecutionHandler:執行緒池對拒絕任務(無執行緒可用)的處理策略。這裡採用了CallerRunsPolicy策略,當執行緒池沒有處理能力的時候,該策略會直接在 execute 方法的呼叫執行緒中執行被拒絕的任務;如果執行程式已關閉,則會丟棄該任務。還有一個是AbortPolicy策略:處理程式遭到拒絕將丟擲執行時RejectedExecutionException。
# 4、異常處理 當方法返回型別為Future時,異常處理很容易– Future.get()方法將丟擲異常。 但是如果是無返回值的非同步方法,異常不會傳播到呼叫執行緒。因此,我們需要新增額外的配置來處理異常。 我們將通過實現AsyncUncaughtExceptionHandler介面來建立自定義非同步異常處理程式。 當存在任何未捕獲的非同步異常時,將呼叫handleUncaughtException()方法: ```java /** * @Author 三分惡 * @Date 2020/7/15 * @Description */ public class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler { @Override public void handleUncaughtException(Throwable throwable, Method method, Object... objects) { System.out.println("Exception message - " + throwable.getMessage()); System.out.println("Method name - " + method.getName()); for (Object param : objects) { System.out.println("Parameter value - " + param); } } } ``` 上面,我們使用配置類實現了AsyncConfigurer介面。 作為其中的一部分,我們還需要重寫getAsyncUncaughtExceptionHandler()方法以返回我們的自定義非同步異常處理: ```java /** * 返回自定義異常處理 * @return */ @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return new CustomAsyncExceptionHandler(); } ```
# 5、總結 這裡非同步請求的使用及相關配置,如超時,異常等處理。在剝離一些和業務無關的操作時,就可以考慮使用非同步呼叫進行其他無關業務操作,以此提供業務的處理效率。或者一些業務場景下可拆分出多個方法進行同步執行又互不影響時,也可以考慮使用非同步呼叫方式提供執行效率。
本文為學習筆記類部落格,學習資料來源見參考!
參考: 【1】:《深入淺出SpringBoot 2.x》 【2】:[Spring Boot中使用@Async實現非同步呼叫](http://blog.didispace.com/springbootasync/) 【3】:[SpringBoot 中非同步執行任務的 2 種方式](http://ckjava.com/2019/08/07/Spring-Boot-EnableAsync-Async-ExecutorService-usage-practice/) 【4】:[How To Do @Async in Spring](https://www.baeldung.com/spring-async) 【5】:[SpringBoot系列:Spring Boot非同步呼叫@Async](https://juejin.im/post/5dc2d60ff265da4d4c202b99) 【6】:[SpringBoot | 第二十一章:非同步開發之非同步呼叫](https://blog.lqdev.cn/2018/08/17/springboot/chapter-twenty-one/) 【7】:[實戰Spring Boot 2.0系列(三) - 使用@Async進行非同步呼叫詳解](https://juejin.im/post/5b27b8366fb9a00e46
“非同步呼叫”對應的是“同步呼叫”, 在實際開發中,有時候為了及時處理請求和進行響應,我們可能使用非同步呼叫,同步呼叫指程式按照定義順序依次執行,每一行程式都必須等待上一行程式執行完成之後才能執行;非同步呼叫指程式在順序執行時,不等待非同步呼叫的語句返回結果就執行後面的程式。非同步呼叫的實現有很多,例如多執行緒、定時任務、訊息佇列等。 這裡學習使用@Async註解來實現非同步呼叫。
# 1、@EnableAsync 首先,我們需要在啟動類上新增 @EnableAsync 註解來宣告開啟非同步方法。 ```java @SpringBootApplication @EnableAsync public class SpringbootAsyncApplication { public static void main(String[] args) { SpringApplication.run(SpringbootAsyncApplication.class, args); } } ```
# 2、@Async 需要注意的,@Async在使用上有一些限制:
## 2.1、無返回值的非同步方法 這是一個非同步執行的無返回值方法: ```java @Async public void asyncMethodWithVoidReturnType() { System.out.println("非同步無返回值方法 " + Thread.currentThread().getName()); } ```
例項:
## 2.1、有返回值的非同步方法 @Async也可以應用有返回值的方法–通過在Future中包裝實際的返回值: ```java /** * 有返回值的非同步方法 * @return */ @Async public Future
例項: * 我們將2.1的例項改造成有返回值的非同步方法: ```java @Async public Future
# 3、 Executor 預設情況下,Spring使用SimpleAsyncTaskExecutor非同步執行這些方法。 可以在兩個級別上重寫預設執行緒池——應用程式級別或方法級別。
## 3.1、方法級別重寫Executor 所需的執行程式需要在配置類中宣告 Executor: ```java /** * @Author 三分惡 * @Date 2020/7/15 * @Description 方法級別重寫執行緒池 */ @Configuration @EnableAsync public class SpringAsyncConfig { @Bean(name = "threadPoolTaskExecutor") public Executor threadPoolTaskExecutor() { return new ThreadPoolTaskExecutor(); } } ``` 然後,在@Async中的屬性提供Executor名稱: ```java @Async("threadPoolTaskExecutor") public void asyncMethodWithConfiguredExecutor() { System.out.println("Execute method with configured executor - " + Thread.currentThread().getName()); } ```
## 3.2、應用級別重寫Executor 配置類應實現AsyncConfigurer介面,重寫getAsyncExecutor()方法。 在這裡,我們將返回整個應用程式的Executor,這樣一來,它就成為執行以@Async註釋的方法的預設Executor: ```java /** * @Author 三分惡 * @Date 2020/7/15 * @Description 應用級別重寫 Excutor */ @Configuration @EnableAsync public class SpringApplicationAsyncConfig implements AsyncConfigurer { @Override public Executor getAsyncExecutor() { return new ThreadPoolTaskExecutor(); } } ```
## 3.3、自定義執行緒池配置 在上面,自定義執行緒池只是簡單地返回了一個執行緒池: ```java return new ThreadPoolTaskExecutor(); ``` 實際上,還可以對執行緒池做一些配置: ```java /** * @Author 三分惡 * @Date 2020/7/15 * @Description */ @Configuration @EnableAsync public class SpringPropertiesAsyncConfig implements AsyncConfigurer { /** * 對執行緒池進行配置 * @return */ @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); taskExecutor.setCorePoolSize(20); taskExecutor.setMaxPoolSize(200); taskExecutor.setQueueCapacity(25); taskExecutor.setKeepAliveSeconds(200); taskExecutor.setThreadNamePrefix("oKong-"); // 執行緒池對拒絕任務(無執行緒可用)的處理策略,目前只支援AbortPolicy、CallerRunsPolicy;預設為後者 taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); taskExecutor.initialize(); return taskExecutor; } } ``` ThreadPoolTaskExecutor配置引數的簡單說明: * corePoolSize:執行緒池維護執行緒的最少數量 * keepAliveSeconds:允許的空閒時間,當超過了核心執行緒出之外的執行緒在空閒時間到達之後會被銷燬 * maxPoolSize:執行緒池維護執行緒的最大數量,只有在緩衝佇列滿了之後才會申請超過核心執行緒數的執行緒 * queueCapacity:快取佇列 * rejectedExecutionHandler:執行緒池對拒絕任務(無執行緒可用)的處理策略。這裡採用了CallerRunsPolicy策略,當執行緒池沒有處理能力的時候,該策略會直接在 execute 方法的呼叫執行緒中執行被拒絕的任務;如果執行程式已關閉,則會丟棄該任務。還有一個是AbortPolicy策略:處理程式遭到拒絕將丟擲執行時RejectedExecutionException。
# 4、異常處理 當方法返回型別為Future時,異常處理很容易– Future.get()方法將丟擲異常。 但是如果是無返回值的非同步方法,異常不會傳播到呼叫執行緒。因此,我們需要新增額外的配置來處理異常。 我們將通過實現AsyncUncaughtExceptionHandler介面來建立自定義非同步異常處理程式。 當存在任何未捕獲的非同步異常時,將呼叫handleUncaughtException()方法: ```java /** * @Author 三分惡 * @Date 2020/7/15 * @Description */ public class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler { @Override public void handleUncaughtException(Throwable throwable, Method method, Object... objects) { System.out.println("Exception message - " + throwable.getMessage()); System.out.println("Method name - " + method.getName()); for (Object param : objects) { System.out.println("Parameter value - " + param); } } } ``` 上面,我們使用配置類實現了AsyncConfigurer介面。 作為其中的一部分,我們還需要重寫getAsyncUncaughtExceptionHandler()方法以返回我們的自定義非同步異常處理: ```java /** * 返回自定義異常處理 * @return */ @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return new CustomAsyncExceptionHandler(); } ```
# 5、總結 這裡非同步請求的使用及相關配置,如超時,異常等處理。在剝離一些和業務無關的操作時,就可以考慮使用非同步呼叫進行其他無關業務操作,以此提供業務的處理效率。或者一些業務場景下可拆分出多個方法進行同步執行又互不影響時,也可以考慮使用非同步呼叫方式提供執行效率。
本文為學習筆記類部落格,學習資料來源見參考!
參考: 【1】:《深入淺出SpringBoot 2.x》 【2】:[Spring Boot中使用@Async實現非同步呼叫](http://blog.didispace.com/springbootasync/) 【3】:[SpringBoot 中非同步執行任務的 2 種方式](http://ckjava.com/2019/08/07/Spring-Boot-EnableAsync-Async-ExecutorService-usage-practice/) 【4】:[How To Do @Async in Spring](https://www.baeldung.com/spring-async) 【5】:[SpringBoot系列:Spring Boot非同步呼叫@Async](https://juejin.im/post/5dc2d60ff265da4d4c202b99) 【6】:[SpringBoot | 第二十一章:非同步開發之非同步呼叫](https://blog.lqdev.cn/2018/08/17/springboot/chapter-twenty-one/) 【7】:[實戰Spring Boot 2.0系列(三) - 使用@Async進行非同步呼叫詳解](https://juejin.im/post/5b27b8366fb9a00e46