Spring定時任務高階使用篇
前面一篇博文 《Spring之定時任務基本使用篇》 介紹了Spring環境下,定時任務的簡單使用姿勢,也留了一些問題,這一篇則希望能針對這些問題給個答案
I. 定時任務進階篇
1. 問題小結
前面一篇博文,丟擲了下面的幾個問題,接下來則圍繞問題進行分析
- 一個專案中有多個定時任務時,他們是並行執行的還是序列執行的?
- 如果預設是序列的
- 那麼有相同的crond表示式的定時任務之間,有先後順序麼?
- 某個任務的阻塞是否會影響後面的任務?
- 如果需要他們並行執行,可以怎麼做?
- 如果是併發執行的
- 是新建立執行緒還是採用執行緒池來複用呢?
- 在併發執行時,假設有個每秒執行一次的任務,但是它執行一次消耗的時間大於1s時,這個任務的表現時怎樣的呢?不斷地新增執行緒來執行還是等執行完畢之後再執行下一次的呢?
2. 多定時任務的串並行分析
如何確認一個專案中的多個定時任務是序列執行還是併發執行呢?要想驗證這個功能,最好的法子就是寫個testcase,比如定義兩個定時任務,在其中一個任務中寫個死迴圈,看另外一個任務是否會正常執行
@Scheduled(cron = "0/1 * * * * ?")
public void sc1() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " | sc1 " + System.currentTimeMillis());
while (true) {
Thread.sleep(5000);
}
}
@Scheduled(cron = "0/1 * * * * ?")
public void sc2() {
System.out.println(Thread.currentThread().getName() + " | sc2 " + System.currentTimeMillis());
}
複製程式碼
首先我們分析的是 sc1和sc2這兩個任務的執行是序列還是並行的,暫時先不考慮 sc1 呼叫時阻塞,下一秒是否是開新的執行緒再呼叫sc1
- 若序列:則sc1列印一次,sc2可能列印0或者1次
- 若並行:sc1列印一次,sc2列印n多次
實際執行,GIF圖演示如下
上圖的結果,印證了預設的情況下,多個定時任務時序列執行的;如果一個任務出現阻塞,其他的任務都會受到影響
3. 定時任務執行的優先順序
既然是順序執行的,那麼優先順序怎麼定?每次都是固定的,還是隨機的呢?
要驗證上面的方法,也容易,同樣兩個任務,看他們的輸出是否會亂掉,如果每次都是任務1列印完再列印任務2,那就是固定優先順序的;否則每次排程時,順序不好說
測試程式碼如下
@Scheduled(cron = "0/1 * * * * ?")
public void sc1() {
System.out.println(Thread.currentThread().getName() + " | sc1 " + System.currentTimeMillis());
}
@Scheduled(cron = "0/1 * * * * ?")
public void sc2() {
System.out.println(Thread.currentThread().getName() + " | sc2 " + System.currentTimeMillis());
}
複製程式碼
實測結果如下
從輸出得出結論:順序是串掉的,並沒有表現出明顯的優先順序關係
4. 並行排程
接下來的問題就是我希望這些任務可以併發執行,可以實現麼?
當然是可以,用起來也比較簡單,首先是在Application上新增註解@EnableAsync
,開啟非同步呼叫,然後再計劃任務上加上@Async
註解即可,一個簡單的demo如下
@EnableAsync
@EnableScheduling
@SpringBootApplication
public class QuickMediaApplication {
public static void main(String[] args) {
SpringApplication.run(QuickMediaApplication.class, args);
}
@Scheduled(cron = "0/1 * * * * ?")
@Async
public void sc1() {
System.out.println(Thread.currentThread().getName() + " | sc1 " + System.currentTimeMillis());
}
}
複製程式碼
上面執行之後,檢視輸出(非同步排程時,理論上執行緒名應該不一樣)
從上面的輸出,可以簡單的推理,每次排程上面的任務都是新開了一個執行緒來做的,所以如果在定時任務中寫了死迴圈,是否會導致無限執行緒,最後整個程式崩掉?
額外提一句,linux系統下單程式的執行緒數是有上線的,檢視命令為:
ulimit -u
複製程式碼
在測試之前,先看下上面的正常任務執行,如下面的動圖,執行緒數並沒有誇張的長法
接下來換成死迴圈的排程方式,實際測試如下,執行緒數蹭蹭的上漲
所以使用預設的非同步呼叫方式,並不是一個好注意,說不準就被玩死了自己都不知道,那麼可以用自己的執行緒池來管理這些非同步任務麼?
5. 自定義執行緒池
用自定義的執行緒池來取代預設執行緒管理方式,無疑是一個更加安全和靈活的方式,使用起來也並不麻煩,和平常建立執行緒池的套路沒什麼區別,要在Spring生態中使用,就把它搞成bean即可
直接藉助Spring的執行緒池ThreadPoolTaskExecutor
@Bean
public AsyncTaskExecutor asyncTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setThreadNamePrefix("yhh-schedule-");
executor.setMaxPoolSize(10);
executor.setCorePoolSize(3);
executor.setQueueCapacity(0);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
return executor;
}
@Scheduled(cron = "0/1 * * * * ?")
@Async
public void sc1() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " | sc1 " + System.currentTimeMillis());
while (true) {
Thread.sleep(1000 * 5);
}
}
複製程式碼
實際演示的結果如下,最多10個執行緒,再提交的任務直接丟棄
簡單說一下,用自定義執行緒池的好處:
- 合理的分配執行緒池引數
- 拒絕策略的選擇也比較有意思(可以按照自己的想法來處理"負載"的任務)
- 執行緒池命名,對於以後問題排查,會有很大的幫助
6. 小結
本來這篇博文在昨天即8月2號就應該寫完的,結果晚上生產環境下除了點問題,解決線上故障之後就比較晚了,留到了今天,哎,拖延症也是要不得。。。
下面小結Spring中定時任務的幾個知識點
- 預設所有的定時任務都是序列排程的,一個執行緒,且即便crond完全相同的兩個任務先後順序也沒法保證(具體原因需要原始碼分析,看下這塊是怎麼支援)
- 使用
@Async
註解可以使定時任務非同步排程;但是需要開啟配置,在啟動類上新增@EnableAsync
註解 - 開啟併發執行時,推薦用自定義的執行緒池來替代預設的,理由見上面
II. 其他
0. 相關
1. 一灰灰Blog: https://liuyueyi.github.io/hexblog
一灰灰的個人部落格,記錄所有學習和工作中的博文,歡迎大家前去逛逛
2. 宣告
盡信書則不如,已上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現bug或者有更好的建議,歡迎批評指正,不吝感激
- 微博地址: 小灰灰Blog
- QQ: 一灰灰/3302797840
3. 掃描關注
小灰灰Blog&公眾號
知識星球