實戰Spring Boot 2.0系列(三) - 使用@Async進行異步調用詳解

前言

異步調用 對應的是 同步調用同步調用 指程序按照 定義順序 依次執行,每一行程序都必須等待上一行程序執行完成以後才能執行;異步調用 指程序在順序執行時,不等待 異步調用的語句 返回結果 就執行後面的程序。java

本系列文章

  1. 實戰Spring Boot 2.0系列(一) - 使用Gradle構建Docker鏡像
  2. 實戰Spring Boot 2.0系列(二) - 全局異常處理和測試
  3. 實戰Spring Boot 2.0系列(三) - 使用@Async進行異步調用詳解
  4. 實戰Spring Boot 2.0系列(四) - 使用WebAsyncTask處理異步任務
  5. 實戰Spring Boot 2.0系列(五) - Listener, Servlet, Filter和Interceptor
  6. 實戰Spring Boot 2.0系列(六) - 單機定時任務的幾種實現

正文

1. 環境準備

利用 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) + "毫秒");
    }
}
複製代碼

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毫秒
複製代碼

任務1、任務2、任務三順序的執行完了,換言之 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();
    }
}
複製代碼
  • 執行單元測試,能夠看到相似以下輸出:
開始作任務三
開始作任務一
開始作任務二
複製代碼

若是反覆執行單元測試,可能會遇到各類不一樣的結果,好比:springboot

  1. 沒有任何任務相關的輸出
  2. 有部分任務相關的輸出
  3. 亂序的任務相關的輸出

緣由是目前 doTaskOne()doTaskTwo()doTaskThree() 這三個方法已經 異步執行 了。主程序在 異步調用 以後,主程序並不會理會這三個函數是否執行完成了,因爲沒有其餘須要執行的內容,因此程序就 自動結束 了,致使了 不完整 或是 沒有輸出任務 相關內容的狀況。bash

注意:@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() 三個方法。循環調用 FutureisDone() 方法等待三個 併發任務 執行完成,記錄最終執行時間。
@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毫秒
複製代碼

能夠看到,經過 異步調用,讓任務1、任務2、任務三 併發執行,有效的 減小 了程序的 運行總時間

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 註解配置 異步任務異步回調任務,包括結合 任務線程池 的使用,以及如何 正確優雅 地關閉 任務線程池


歡迎關注技術公衆號: 零壹技術棧

零壹技術棧

本賬號將持續分享後端技術乾貨,包括虛擬機基礎,多線程編程,高性能框架,異步、緩存和消息中間件,分佈式和微服務,架構學習和進階等學習資料和文章。

相關文章
相關標籤/搜索