SpringBoot異步調用

除了異步請求,一般上我們用的比較多的應該是異步調用。通常在開發過程中,會遇到一個方法是和實際業務無關的,沒有緊密性的。比如記錄日誌信息等業務。這個時候正常就是啓一個新線程去做一些業務處理,讓主線程異步的執行其他業務。

何爲異步調用

說異步調用前,我們說說它對應的同步調用。通常開發過程中,一般上我們都是同步調用,即:程序按定義的順序依次執行的過程,每一行代碼執行過程必須等待上一行代碼執行完畢後才執行。而異步調用指:程序在執行時,無需等待執行的返回值可繼續執行後面的代碼。顯而易見,同步有依賴相關性,而異步沒有,所以異步可併發執行,可提高執行效率,在相同的時間做更多的事情。

題外話:除了異步、同步外,還有一個叫回調。其主要是解決異步方法執行結果的處理方法,比如在希望異步調用結束時返回執行結果,這個時候就可以考慮使用回調機制。

Async異步調用

在SpringBoot中使用異步調用是很簡單的,只需要使用@Async註解即可實現方法的異步調用。

注意:需要在啓動類加入@EnableAsync使異步調用@Async註解生效。使用@Async很簡單,只需要在需要異步執行的方法上加入此註解即可。這裏創建一個控制層和一個服務層,進行簡單示例下。

@Slf4j
@Service
public class SyncService {
    @Async
    public void asyncEvent() throws InterruptedException {
        //休眠1s
        TimeUnit.SECONDS.sleep(1);
        log.info("異步方法內部線程名稱:{}!", Thread.currentThread().getName());
    }
    public void syncEvent() throws InterruptedException {
        TimeUnit.SECONDS.sleep(1);
        log.info("同步方法內部線程名稱:{}!", Thread.currentThread().getName());
    }
}
@Slf4j
@RestController
public class AsyncController {
    @Autowired
    private SyncService syncService;

    @GetMapping("/async")
    public String doAsync() throws InterruptedException, ExecutionException {
        long start = System.currentTimeMillis();
        //調用同步方法
        syncService.syncEvent();
        long syncEndTime = System.currentTimeMillis();
        log.info("同步方法用時:{}", syncEndTime - start);
        //調用異步方法
        syncService.asyncEvent();//異步方法無返回值
        long asyncEndTime = System.currentTimeMillis();
        log.info("異步方法用時:{}", asyncEndTime - syncEndTime);
        return "async!!!";
    }

}
@SpringBootApplication
@EnableAsync
public class SpringWebApplication{

    public static void main(String[] args) {
        SpringApplication.run(SpringWebApplication.class, args);
    }
}

應用啓動後,可以看見控制檯輸出:

 

可以看出,調用異步方法時,是立即返回的,基本沒有耗時。

這裏有幾點需要注意下:

  1. 在默認情況下,未設置TaskExecutor時,默認是使用SimpleAsyncTaskExecutor這個線程池,但此線程不是真正意義上的線程池,因爲線程不重用,每次調用都會創建一個新的線程。可通過控制檯日誌輸出可以看出,每次輸出線程名都是遞增的。
  2. 調用的異步方法,不能爲同一個類的方法,簡單來說,因爲Spring在啓動掃描時會爲其創建一個代理類,而同類調用時,還是調用本身的代理類的,所以和平常調用是一樣的。其他的註解如@Cache等也是一樣的道理,說白了,就是Spring的代理機製造成的。

自定義線程池

前面有提到,在默認情況下,系統使用的是默認的SimpleAsyncTaskExecutor進行線程創建。所以一般上我們會自定義線程池來進行線程的複用。

@Configuration
public class Config {
    /**
     * 配置線程池
     * @return
     */
    @Bean(name = "asyncPoolTaskExecutor")
    public ThreadPoolTaskExecutor asyncThreadPoolTaskExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(20);
        taskExecutor.setMaxPoolSize(200);
        taskExecutor.setQueueCapacity(25);
        taskExecutor.setKeepAliveSeconds(200);
        taskExecutor.setThreadNamePrefix("MyExecutor-");
        // 線程池對拒絕任務(無線程可用)的處理策略,目前只支持AbortPolicy、CallerRunsPolicy;默認爲後者
        taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        taskExecutor.initialize();
        return taskExecutor;
    }
}

此時,使用的是就只需要在@Async加入線程池名稱即可:

@Async("asyncPoolTaskExecutor")
public void asyncEvent() throws InterruptedException {
    //休眠1s
    TimeUnit.SECONDS.sleep(1);
    log.info("異步方法內部線程名稱:{}!", Thread.currentThread().getName());
}

這裏簡單說明下,關於ThreadPoolTaskExecutor參數說明:

  1. corePoolSize:線程池維護線程的最少數量
  2. keepAliveSeconds:允許的空閒時間,當超過了核心線程數之外的線程在空閒時間到達之後會被銷燬
  3. maxPoolSize:線程池維護線程的最大數量,只有在緩衝隊列滿了之後纔會申請超過核心線程數的線程
  4. queueCapacity:緩存隊列
  5. rejectedExecutionHandler:線程池對拒絕任務(無線程可用)的處理策略。這裏採用了CallerRunsPolicy策略,當線程池沒有處理能力的時候,該策略會直接在 execute 方法的調用線程中運行被拒絕的任務;如果執行程序已關閉,則會丟棄該任務。還有一個是AbortPolicy策略:處理程序遭到拒絕將拋出運行時RejectedExecutionException。

而在一些場景下,若需要在關閉線程池時等待當前調度任務完成後纔開始關閉,可以通過簡單的配置,進行優雅的停機策略配置。關鍵就是通過setWaitForTasksToCompleteOnShutdown(true)和setAwaitTerminationSeconds方法。

  • setWaitForTasksToCompleteOnShutdown:表明等待所有線程執行完,默認爲false。
  • setAwaitTerminationSeconds:等待的時間,因爲不能無限的等待下去。

所以,線程池完整配置爲:

@Bean(name = "asyncPoolTaskExecutor")
    public ThreadPoolTaskExecutor asyncThreadPoolTaskExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(20);
        taskExecutor.setMaxPoolSize(200);
        taskExecutor.setQueueCapacity(25);
        taskExecutor.setKeepAliveSeconds(200);
        taskExecutor.setThreadNamePrefix("MyExecutor-");
        // 線程池對拒絕任務(無線程可用)的處理策略,目前只支持AbortPolicy、CallerRunsPolicy;默認爲後者
        taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        //調度器shutdown被調用時等待當前被調度的任務完成
        taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
        //等待時長
        taskExecutor.setAwaitTerminationSeconds(60);
        taskExecutor.initialize();
        return taskExecutor;
    }

配置默認的線程池

如果我們想使用默認的線程池,但是隻是想修改默認線程池的配置,那怎麼做呢,此時我們需要實現AsyncConfigurer類,示例代碼如下:

@Configuration
@EnableAsync
public class TaskExecutorConfig implements AsyncConfigurer {
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return null;
    }
    /**
     * 配置線程池
     * @return
     */

    public ThreadPoolTaskExecutor getAsyncExecutor() {
       ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(20);
        taskExecutor.setMaxPoolSize(200);
        taskExecutor.setQueueCapacity(25);
        taskExecutor.setKeepAliveSeconds(200);
        taskExecutor.setThreadNamePrefix("MyExecutor-");//線程名稱前綴
        // 線程池對拒絕任務(無線程可用)的處理策略,目前只支持AbortPolicy、CallerRunsPolicy;默認爲後者
        taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        //調度器shutdown被調用時等待當前被調度的任務完成
        taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
        //等待時長
        taskExecutor.setAwaitTerminationSeconds(60);
        taskExecutor.initialize();
        return taskExecutor;
    }

}

異步回調及超時處理

對於一些業務場景下,需要異步回調的返回值時,就需要使用異步回調來完成了。主要就是通過Future進行異步回調。

@Async
public Future<String> asyncEventWithReturn() throws InterruptedException {
    //休眠1s
    TimeUnit.SECONDS.sleep(1);
    log.info("異步方法內部線程名稱:{}!", Thread.currentThread().getName());
    return new AsyncResult<>("異步方法有返回值");
}

其中AsyncResult是Spring提供的一個Future接口的子類。

然後通過isDone方法,判斷是否已經執行完畢。

@Slf4j
@RestController
public class AsyncController {
    @Autowired
    private SyncService syncService;

@GetMapping("/async_return")
public String doAsyncWithReturn() throws InterruptedException, ExecutionException {
    long start = System.currentTimeMillis();
    //調用同步方法
    syncService.syncEvent();
    long syncEndTime = System.currentTimeMillis();
    log.info("同步方法用時:{}", syncEndTime - start);
    //調用異步方法
    // 異步方法有返回值
    Future<String> doFutrue = syncService.asyncEventWithReturn();
    while (true) {
        //判斷異步任務是否完成
        if (doFutrue.isDone()) {
            log.info("異步回調結果:" + doFutrue.get());
            break;
        }
        Thread.sleep(100);
    }
    long endTime = System.currentTimeMillis();
    log.info("異步方法用時:{}", endTime - syncEndTime);
    return doFutrue.get();
}

}

所以,當某個業務功能可以同時拆開一起執行時,可利用異步回調機制,可有效的減少程序執行時間,提高效率。

超時處理

對於一些需要異步回調的函數,不能無期限的等待下去,所以一般上需要設置超時時間,超時後可將線程釋放,而不至於一直堵塞而佔用資源。

對於Future配置超時,很簡單,通過get方法即可,具體如下:

//get方法會一直堵塞,直到等待執行完成才返回

//get(long timeout, TimeUnit unit) 在設置時間類未返回結果,會直接排除異常TimeoutException,messages爲null

String result = doFutrue.get(60, TimeUnit.SECONDS);//60s

超時後,會拋出異常TimeoutException類,此時可進行統一異常捕獲即可。

 

參考:

https://blog.csdn.net/liuchuanhong1/article/details/64132520

https://my.oschina.net/xiedeshou/blog/1929325

發佈了151 篇原創文章 · 獲贊 184 · 訪問量 44萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章