前言
非同步呼叫 對應的是 同步呼叫,同步呼叫 指程式按照 定義順序 依次執行,每一行程式都必須等待上一行程式執行完成之後才能執行;非同步呼叫 指程式在順序執行時,不等待 非同步呼叫的語句 返回結果 就執行後面的程式。
本系列文章
- 實戰Spring Boot 2.0系列(一) - 使用Gradle構建Docker映象
- 實戰Spring Boot 2.0系列(二) - 全域性異常處理和測試
- 實戰Spring Boot 2.0系列(三) - 使用@Async進行非同步呼叫詳解
- 實戰Spring Boot 2.0系列(四) - 使用WebAsyncTask處理非同步任務
- 實戰Spring Boot 2.0系列(五) - Listener, Servlet, Filter和Interceptor
- 實戰Spring Boot 2.0系列(六) - 單機定時任務的幾種實現
正文
1. 環境準備
利用 Spring Initializer
建立一個 gradle
專案 spring-boot-async-task
,建立時新增相關依賴。得到的初始 build.gradle
如下:
buildscript {
ext {
springBootVersion = '2.0.3.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
group = 'io.ostenant.springboot.sample'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
compileOnly('org.projectlombok:lombok')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
複製程式碼
在 Spring Boot
入口類上配置 @EnableAsync
註解開啟非同步處理。
@SpringBootApplication
@EnableAsync
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
複製程式碼
建立任務抽象類 AbstractTask
,並分別配置三個任務方法 doTaskOne()
,doTaskTwo()
,doTaskThree()
。
public abstract class AbstractTask {
private static Random random = new Random();
public void doTaskOne() throws Exception {
out.println("開始做任務一");
long start = currentTimeMillis();
sleep(random.nextInt(10000));
long end = currentTimeMillis();
out.println("完成任務一,耗時:" + (end - start) + "毫秒");
}
public void doTaskTwo() throws Exception {
out.println("開始做任務二");
long start = currentTimeMillis();
sleep(random.nextInt(10000));
long end = currentTimeMillis();
out.println("完成任務二,耗時:" + (end - start) + "毫秒");
}
public void doTaskThree() throws Exception {
out.println("開始做任務三");
long start = currentTimeMillis();
sleep(random.nextInt(10000));
long end = currentTimeMillis();
out.println("完成任務三,耗時:" + (end - start) + "毫秒");
}
}
複製程式碼
2. 同步呼叫
下面通過一個簡單示例來直觀的理解什麼是同步呼叫:
- 定義
Task
類,繼承AbstractTask
,三個處理函式分別模擬三個執行任務的操作,操作消耗時間隨機取(10
秒內)。
@Component
public class Task extends AbstractTask {
}
複製程式碼
- 在 單元測試 用例中,注入
Task
物件,並在測試用例中執行doTaskOne()
,doTaskTwo()
,doTaskThree()
三個方法。
@RunWith(SpringRunner.class)
@SpringBootTest
public class TaskTest {
@Autowired
private Task task;
@Test
public void testSyncTasks() throws Exception {
task.doTaskOne();
task.doTaskTwo();
task.doTaskThree();
}
}
複製程式碼
- 執行單元測試,可以看到類似如下輸出:
開始做任務一
完成任務一,耗時:4059毫秒
開始做任務二
完成任務二,耗時:6316毫秒
開始做任務三
完成任務三,耗時:1973毫秒
複製程式碼
任務一、任務二、任務三順序的執行完了,換言之 doTaskOne()
,doTaskTwo()
,doTaskThree()
三個方法順序的執行完成。
3. 非同步呼叫
上述的 同步呼叫 雖然順利的執行完了三個任務,但是可以看到 執行時間比較長,若這三個任務本身之間 不存在依賴關係,可以 併發執行 的話,同步呼叫在 執行效率 方面就比較差,可以考慮通過 非同步呼叫 的方式來 併發執行。
- 建立
AsyncTask
類,分別在方法上配置@Async
註解,將原來的 同步方法 變為 非同步方法。
@Component
public class AsyncTask extends AbstractTask {
@Async
public void doTaskOne() throws Exception {
super.doTaskOne();
}
@Async
public void doTaskTwo() throws Exception {
super.doTaskTwo();
}
@Async
public void doTaskThree() throws Exception {
super.doTaskThree();
}
}
複製程式碼
- 在 單元測試 用例中,注入
AsyncTask
物件,並在測試用例中執行doTaskOne()
,doTaskTwo()
,doTaskThree()
三個方法。
@RunWith(SpringRunner.class)
@SpringBootTest
public class AsyncTaskTest {
@Autowired
private AsyncTask task;
@Test
public void testAsyncTasks() throws Exception {
task.doTaskOne();
task.doTaskTwo();
task.doTaskThree();
}
}
複製程式碼
- 執行單元測試,可以看到類似如下輸出:
開始做任務三
開始做任務一
開始做任務二
複製程式碼
如果反覆執行單元測試,可能會遇到各種不同的結果,比如:
- 沒有任何任務相關的輸出
- 有部分任務相關的輸出
- 亂序的任務相關的輸出
原因是目前 doTaskOne()
,doTaskTwo()
,doTaskThree()
這三個方法已經 非同步執行 了。主程式在 非同步呼叫 之後,主程式並不會理會這三個函式是否執行完成了,由於沒有其他需要執行的內容,所以程式就 自動結束 了,導致了 不完整 或是 沒有輸出任務 相關內容的情況。
注意:@Async所修飾的函式不要定義為static型別,這樣非同步呼叫不會生效。
4. 非同步回撥
為了讓 doTaskOne()
,doTaskTwo()
,doTaskThree()
能正常結束,假設我們需要統計一下三個任務 併發執行 共耗時多少,這就需要等到上述三個函式都完成動用之後記錄時間,並計算結果。
那麼我們如何判斷上述三個 非同步呼叫 是否已經執行完成呢?我們需要使用 Future<T>
來返回 非同步呼叫 的 結果。
- 建立
AsyncCallBackTask
類,宣告doTaskOneCallback()
,doTaskTwoCallback()
,doTaskThreeCallback()
三個方法,對原有的三個方法進行包裝。
@Component
public class AsyncCallBackTask extends AbstractTask {
@Async
public Future<String> doTaskOneCallback() throws Exception {
super.doTaskOne();
return new AsyncResult<>("任務一完成");
}
@Async
public Future<String> doTaskTwoCallback() throws Exception {
super.doTaskTwo();
return new AsyncResult<>("任務二完成");
}
@Async
public Future<String> doTaskThreeCallback() throws Exception {
super.doTaskThree();
return new AsyncResult<>("任務三完成");
}
}
複製程式碼
- 在 單元測試 用例中,注入
AsyncCallBackTask
物件,並在測試用例中執行doTaskOneCallback()
,doTaskTwoCallback()
,doTaskThreeCallback()
三個方法。迴圈呼叫Future
的isDone()
方法等待三個 併發任務 執行完成,記錄最終執行時間。
@RunWith(SpringRunner.class)
@SpringBootTest
public class AsyncCallBackTaskTest {
@Autowired
private AsyncCallBackTask task;
@Test
public void testAsyncCallbackTask() throws Exception {
long start = currentTimeMillis();
Future<String> task1 = task.doTaskOneCallback();
Future<String> task2 = task.doTaskTwoCallback();
Future<String> task3 = task.doTaskThreeCallback();
// 三個任務都呼叫完成,退出迴圈等待
while (!task1.isDone() || !task2.isDone() || !task3.isDone()) {
sleep(1000);
}
long end = currentTimeMillis();
out.println("任務全部完成,總耗時:" + (end - start) + "毫秒");
}
}
複製程式碼
看看都做了哪些改變:
- 在測試用例一開始記錄開始時間;
- 在呼叫三個非同步函式的時候,返回Future型別的結果物件;
- 在呼叫完三個非同步函式之後,開啟一個迴圈,根據返回的Future物件來判斷三個非同步函式是否都結束了。若都結束,就結束迴圈;若沒有都結束,就等1秒後再判斷。
- 跳出迴圈之後,根據結束時間 - 開始時間,計算出三個任務併發執行的總耗時。
執行一下上述的單元測試,可以看到如下結果:
開始做任務一
開始做任務三
開始做任務二
完成任務二,耗時:4882毫秒
完成任務三,耗時:6484毫秒
完成任務一,耗時:8748毫秒
任務全部完成,總耗時:9043毫秒
複製程式碼
可以看到,通過 非同步呼叫,讓任務一、任務二、任務三 併發執行,有效的 減少 了程式的 執行總時間。
5. 定義執行緒池
在上述操作中,建立一個 執行緒池配置類 TaskConfiguration
,並配置一個 任務執行緒池物件 taskExecutor
。
@Configuration
public class TaskConfiguration {
@Bean("taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(200);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("taskExecutor-");
executor.setRejectedExecutionHandler(new CallerRunsPolicy());
return executor;
}
}
複製程式碼
上面我們通過使用 ThreadPoolTaskExecutor
建立了一個 執行緒池,同時設定了以下這些引數:
執行緒池屬性 | 屬性的作用 | 設定初始值 |
---|---|---|
核心執行緒數 | 執行緒池建立時候初始化的執行緒數 | 10 |
最大執行緒數 | 執行緒池最大的執行緒數,只有在緩衝佇列滿了之後,才會申請超過核心執行緒數的執行緒 | 20 |
緩衝佇列 | 用來緩衝執行任務的佇列 | 200 |
允許執行緒的空閒時間 | 當超過了核心執行緒之外的執行緒,在空閒時間到達之後會被銷燬 | 60秒 |
執行緒池名的字首 | 可以用於定位處理任務所在的執行緒池 | taskExecutor- |
執行緒池對拒絕任務的處理策略 | 這裡採用CallerRunsPolicy策略,當執行緒池沒有處理能力的時候,該策略會直接在execute方法的呼叫執行緒中執行被拒絕的任務;如果執行程式已關閉,則會丟棄該任務 | CallerRunsPolicy |
- 建立
AsyncExecutorTask
類,三個任務的配置和AsyncTask
一樣,不同的是@Async
註解需要指定前面配置的 執行緒池的名稱taskExecutor
。
@Component
public class AsyncExecutorTask extends AbstractTask {
@Async("taskExecutor")
public void doTaskOne() throws Exception {
super.doTaskOne();
out.println("任務一,當前執行緒:" + currentThread().getName());
}
@Async("taskExecutor")
public void doTaskTwo() throws Exception {
super.doTaskTwo();
out.println("任務二,當前執行緒:" + currentThread().getName());
}
@Async("taskExecutor")
public void doTaskThree() throws Exception {
super.doTaskThree();
out.println("任務三,當前執行緒:" + currentThread().getName());
}
}
複製程式碼
- 在 單元測試 用例中,注入
AsyncExecutorTask
物件,並在測試用例中執行doTaskOne()
,doTaskTwo()
,doTaskThree()
三個方法。
@RunWith(SpringRunner.class)
@SpringBootTest
public class AsyncExecutorTaskTest {
@Autowired
private AsyncExecutorTask task;
@Test
public void testAsyncExecutorTask() throws Exception {
task.doTaskOne();
task.doTaskTwo();
task.doTaskThree();
sleep(30 * 1000L);
}
}
複製程式碼
執行一下上述的 單元測試,可以看到如下結果:
開始做任務一
開始做任務三
開始做任務二
完成任務二,耗時:3905毫秒
任務二,當前執行緒:taskExecutor-2
完成任務一,耗時:6184毫秒
任務一,當前執行緒:taskExecutor-1
完成任務三,耗時:9737毫秒
任務三,當前執行緒:taskExecutor-3
複製程式碼
執行上面的單元測試,觀察到 任務執行緒池 的 執行緒池名的字首 被列印,說明 執行緒池 成功執行 非同步任務!
6. 優雅地關閉執行緒池
由於在應用關閉的時候非同步任務還在執行,導致類似 資料庫連線池 這樣的物件一併被 銷燬了,當 非同步任務 中對 資料庫 進行操作就會出錯。
解決方案如下,重新設定執行緒池配置物件,新增執行緒池 setWaitForTasksToCompleteOnShutdown()
和 setAwaitTerminationSeconds()
配置:
@Bean("taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskScheduler executor = new ThreadPoolTaskScheduler();
executor.setPoolSize(20);
executor.setThreadNamePrefix("taskExecutor-");
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
return executor;
}
複製程式碼
-
setWaitForTasksToCompleteOnShutdown(true): 該方法用來設定 執行緒池關閉 的時候 等待 所有任務都完成後,再繼續 銷燬 其他的
Bean
,這樣這些 非同步任務 的 銷燬 就會先於 資料庫連線池物件 的銷燬。 -
setAwaitTerminationSeconds(60): 該方法用來設定執行緒池中 任務的等待時間,如果超過這個時間還沒有銷燬就 強制銷燬,以確保應用最後能夠被關閉,而不是阻塞住。
小結
本文介紹了在 Spring Boot
中如何使用 @Async
註解配置 非同步任務、非同步回撥任務,包括結合 任務執行緒池 的使用,以及如何 正確 並 優雅 地關閉 任務執行緒池。
歡迎關注技術公眾號: 零壹技術棧
本帳號將持續分享後端技術乾貨,包括虛擬機器基礎,多執行緒程式設計,高效能框架,非同步、快取和訊息中介軟體,分散式和微服務,架構學習和進階等學習資料和文章。