異步調用 對應的是 同步調用,同步調用 指程序按照 定義順序 依次執行,每一行程序都必須等待上一行程序執行完成以後才能執行;異步調用 指程序在順序執行時,不等待 異步調用的語句 返回結果 就執行後面的程序。java
利用 Spring Initializer
建立一個 gradle
項目 spring-boot-async-task
,建立時添加相關依賴。獲得的初始 build.gradle
以下:web
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
註解開啓異步處理。spring
@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) + "毫秒");
}
}
複製代碼
下面經過一個簡單示例來直觀的理解什麼是同步調用:編程
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毫秒
複製代碼
任務1、任務2、任務三順序的執行完了,換言之 doTaskOne()
,doTaskTwo()
,doTaskThree()
三個方法順序的執行完成。後端
上述的 同步調用 雖然順利的執行完了三個任務,可是能夠看到 執行時間比較長,若這三個任務自己之間 不存在依賴關係,能夠 併發執行 的話,同步調用在 執行效率 方面就比較差,能夠考慮經過 異步調用 的方式來 併發執行。緩存
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();
}
}
複製代碼
開始作任務三
開始作任務一
開始作任務二
複製代碼
若是反覆執行單元測試,可能會遇到各類不一樣的結果,好比:springboot
緣由是目前 doTaskOne()
,doTaskTwo()
,doTaskThree()
這三個方法已經 異步執行 了。主程序在 異步調用 以後,主程序並不會理會這三個函數是否執行完成了,因爲沒有其餘須要執行的內容,因此程序就 自動結束 了,致使了 不完整 或是 沒有輸出任務 相關內容的狀況。bash
注意:@Async所修飾的函數不要定義爲static類型,這樣異步調用不會生效。多線程
爲了讓 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) + "毫秒");
}
}
複製代碼
看看都作了哪些改變:
執行一下上述的單元測試,能夠看到以下結果:
開始作任務一
開始作任務三
開始作任務二
完成任務二,耗時:4882毫秒
完成任務三,耗時:6484毫秒
完成任務一,耗時:8748毫秒
任務所有完成,總耗時:9043毫秒
複製代碼
能夠看到,經過 異步調用,讓任務1、任務2、任務三 併發執行,有效的 減小 了程序的 運行總時間。
在上述操做中,建立一個 線程池配置類 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
複製代碼
執行上面的單元測試,觀察到 任務線程池 的 線程池名的前綴 被打印,說明 線程池 成功執行 異步任務!
因爲在應用關閉的時候異步任務還在執行,致使相似 數據庫鏈接池 這樣的對象一併被 銷燬了,當 異步任務 中對 數據庫 進行操做就會出錯。
解決方案以下,從新設置線程池配置對象,新增線程池 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
註解配置 異步任務、異步回調任務,包括結合 任務線程池 的使用,以及如何 正確 並 優雅 地關閉 任務線程池。
歡迎關注技術公衆號: 零壹技術棧
本賬號將持續分享後端技術乾貨,包括虛擬機基礎,多線程編程,高性能框架,異步、緩存和消息中間件,分佈式和微服務,架構學習和進階等學習資料和文章。