為什麼你的 Spring Task 定時任務沒有定時執行?

前言

定時任務的使用,在開發中可謂是家常便飯了,定時發送郵件、短信。 避免數據庫,數據表過大,定時將數據轉儲。通知、對賬等等。

當然實現定時任務的方式也有很多,比如使用 linux 下的 contab 腳本,jdk 中自帶的 Timer 類。Spring Task 或是 Quartz 。

相信你也有過如下的疑問:

  • Spring Task 的 contab 的表達式 和 linux 下的 contab 有什麼區別?
  • crontab 表達式記不住?
  • 定時任務阻塞會有什麼影響?
  • 多個定時任務的情況下是如何運行的?
  • 具有相同表達式的定時任務,他們的執行順序如何?
  • 為什麼async異步任務沒有生效?

所以這篇文章,我們來介紹一下,在 Spring Task 中, 定時任務的執行原理及相關問題。演示環境為 Spring Boot 項目。

SpringBoot 定時任務的原理

相信絕大部分開發者都使用過Spring Boot 為我們提供的定時任務的 Starter 和定時任務的註解。所以我們來主要介紹一下 Spring Boot 實現定時任務的原理,和其相關注解的作用。

Spring 在 3.0版本後通過 @Scheduled 註解來完成對定時任務的支持。

為什麼你的 Spring Task 定時任務沒有定時執行?

在我們使用時,需要在Application 啟動類上加上 @EnableScheduling 註解,它是從Spring 3.1後開始提供的。

為什麼你的 Spring Task 定時任務沒有定時執行?

由於現在 Spring3 版本較低,使用得比較少了,可能並不會考慮太多細節,大多隻需要關注目標實現,所以我們在配套使用兩個註解的時候,並不會出現什麼問題。

在3.0 中 ,是通過

<code>  

<executor>

<scheduler>

<annotation-driven> executor="executor" proxy-target-class="true" />
/<annotation-driven>/<code>

上述的 XML 配置 和 @Scheduled 配合實現定時任務的,而我們這裡的 @EnableScheduling 其實類似的和它等價,是用來發現註解了 @Scheduled 的方法,沒有這個註解光有 @Scheduled 是無法執行的,大家可以做一個簡單案例測試一下,其底層是 Spring 自己實現的一套定時任務的處理邏輯,所以使用起來比較簡單。

任務一直阻塞會怎麼樣?

介紹了兩個註解的作用後,我們來開始做實驗,簡單的寫一個定時執行的方法。

為什麼你的 Spring Task 定時任務沒有定時執行?

每隔 20s 輸出一句話,在輸出幾行記錄後,打上了一個斷點。

對後續的任務有什麼影響呢?

為什麼你的 Spring Task 定時任務沒有定時執行?

可以看到,斷點時的後續任務是阻塞著的,從圖上,我們還可以看出初始化的名為pool-1-thread-1 的線程池同樣證實了我們的想法,線程池中只有一個線程,創建方法是:

<code>Executors.newSingleThreadScheduledExecutor();
/<code>

從這個例子來看,斷點時,任務會一直阻塞,當阻塞恢復後,會立馬執行阻塞的任務。線程池內部時採用 DelayQueue 延遲隊列實現的,它的特點是: 無界、延遲、阻塞的一種隊列,能按一定的順序對工作隊列中的元素進行排列。

為什麼你的 Spring Task 定時任務沒有定時執行?

多個定時任務的執行

通過上面的實驗,我們知道,咋看默認情況下,任務的線程池,只會有一個線程來執行任務,如果有多個定時任務,它們也應該是串行執行的。

為什麼你的 Spring Task 定時任務沒有定時執行?

從上圖可以看出,一旦線程執行任務1後,就會睡眠2分鐘。線程在死循環內部一直處於Running 狀態。

為什麼你的 Spring Task 定時任務沒有定時執行?

通過觀察日誌,根本沒有任務2的輸出,我們知道默認情況下,多個定時任務是串行執行的,類似於多輛車過單行道的橋,如果一個任務出現阻塞,其他的任務都會受到影響。

那如果線程池包含多個線程的情況下,多個定時任務併發的情況是什麼樣?

為什麼你的 Spring Task 定時任務沒有定時執行?

串行當然很好理解,就是上文說的汽車過橋,依次通過。再來理解併發,區別於並行,併發是指一個處理器同時處理多個任務,而並行是指多個(核)處理器同時處理多個不同的任務。併發不一定同一時間發生,而並行,指的是同一時間。

具有相同表達式的定時任務,他們的執行順序如何?

從上面的實驗同樣能知道,具有相同表達式的定時任務,還是和調度有關,如果是默認的線程池,那麼會串行執行,首先獲取到cpu時間片的先執行。在多線程情況下,具體的先後執行順序和線程池線程數和所用線程池所用隊列等等因素有關。

Spring Task和linux crontab的cron語法區別?

兩者的 cron 表達式其實很相似,需要注意的是 linux 的contab 只為我們提供了最小顆粒度為分鐘級的任務,而java中最小的粒度是從秒開始的。具體細節如下圖:

為什麼你的 Spring Task 定時任務沒有定時執行?

在cron語法中容易犯的錯誤

以spring 中的task為例,cron 表達式中 "/" 代表每的意思,“*/10”表示每10個單位。

在cron 語法 中很多人會犯錯誤。比如要求寫出每十分鐘定時執行的 cron 語句,可能會有以下版本的出現:

為什麼你的 Spring Task 定時任務沒有定時執行?

所以當我們寫完cron 表達式的時候,可以適當的調低執行間隔時間來測試,或是通過一些在線的網站來檢測你的cron腳本是否正確。

@Async異步註解原理及作用

Spring task中 和異步相關的註解有兩個 , 一個是 @EnableAsync ,另一個就是 @Async 。

為什麼你的 Spring Task 定時任務沒有定時執行?

首先我們單純的在方法上引入 @Async 異步註解,並且打印當前線程的名稱,實驗後發現,方法仍然是由一個線程來同步執行的。

和@schedule 類似 還是通過@Enable開頭的註解來控制執行的。我們在啟動類上加入@EnableAsync 後再觀察輸出內容。

為什麼你的 Spring Task 定時任務沒有定時執行?

可以發現,默認情況下,其內部是使用的名為SimpleAsyncTaskExecutor的線程池來執行任務,而且每一次任務調度,都會新建一個線程。

使用@EnableAsync註解開啟了Spring的異步功能,Spring會按照如下的方式查找相應的線程池用於執行異步方法: 查找實現了TaskExecutor接口的Bean實例。

如果上面沒有找到,則查找名稱為taskExecutor並且實現了Executor接口的Bean實例。

如果還是沒有找到,則使用SimpleAsyncTaskExecutor,該實現每次都會創建一個新的線程執行任務。

併發執行任務如何配置?

方式一,我們可以將默認的線程池替換為我們自定義的線程池。通過ScheduleConfig配置文件實現SchedulingConfigurer接口,並重寫setSchedulerfang方法。

可實現AsyncConfigurer接口複寫getAsyncExecutor獲取異步執行器,getAsyncUncaughtExceptionHandler獲取異步未捕獲異常處理器

<code>@Configurationpublic
class ScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(Executors.newScheduledThreadPool(5));
}
}
/<code>

方式二:不改變任務調度器默認使用的線程池,而是把當前任務交給一個異步線程池去執行。

<code>  @Scheduled(fixedRate = 1000*10,initialDelay = 1000*20)
@Async("hyqThreadPoolTaskExecutor")
public void test(){
System.out.println(Thread.currentThread().getName()+"--->xxxxx--->"+Thread.currentThread().getId());
}

//自定義線程池
@Bean(name = "hyqThreadPoolTaskExecutor")
public TaskExecutor getMyThreadPoolTaskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(20);
taskExecutor.setMaxPoolSize(200);
taskExecutor.setQueueCapacity(25);
taskExecutor.setKeepAliveSeconds(200);
taskExecutor.setThreadNamePrefix("hyq-threadPool-");

taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
taskExecutor.setAwaitTerminationSeconds(60);
taskExecutor.initialize();
return taskExecutor;
}
/<code>

其他問題

如果是定時任務沒有生效,需要檢查 @EnableScheduling 註解是否加上。 如果是異步沒有生效,需要檢查 @EnableAsync 註解是否加上,並且定義線程池,否則仍然是串行執行的。

總結

文章介紹了SpringBoot 定時任務的原理, 3.0版本前後的區別,通過單線程任務阻塞實驗,探究了延遲隊列及串行、並行、併發的概念。對比了linux下的 contab 和spring的cron表達式區別以及常犯的錯誤。最後通過實驗異步註解,兩種方式配置線程池,讓任務高效運作,希望本文能讓你有所收穫。

創建了一個java方面的互助群,和其他傳統的學習群不同。

本群主要致力於解決項目中的疑難問題,在遇到項目難以解決的

在本群,你可以

1)闡述你在開發過程中遇到的問題,群友集思廣益,高效解答。

2)分享自己學習的一些心得,讓後來學習者少踩坑。

3)資源共享,無論是好的學習視頻還是文檔都可以在群內共享。

別人有可能可以給你提供一些思路和看法

同樣,如果你也樂於幫助別人,那解決別人遇到的問題,也同樣對你是一種鍛鍊。

想邀你加入這個有溫度的社群

分享經驗和心得

集思廣益,高效解答問題

幫助他人,鍛鍊自己


分享到:


相關文章: