180803-Spring定時任務高級使用篇


logo

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圖演示如下

sch01.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());
}

實測結果如下

sch02.jpg

從輸出得出結論:順序是串掉的,並沒有表現出明顯的優先級關系

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());
    }
}

上面執行之后,查看輸出(異步調度時,理論上線程名應該不一樣)

sch03.jpg

從上面的輸出,可以簡單的推理,每次調度上面的任務都是新開了一個線程來做的,所以如果在定時任務中寫了死循環,是否會導致無限線程,最后整個進程崩掉?

額外提一句,linux系統下單進程的線程數是有上線的,查看命令為:

ulimit -u

在測試之前,先看下上面的正常任務執行,如下面的動圖,線程數並沒有誇張的長法

sch04.gif

接下來換成死循環的調度方式,實際測試如下,線程數蹭蹭的上漲

sch05.gif

所以使用默認的異步調用方式,並不是一個好注意,說不准就被玩死了自己都不知道,那么可以用自己的線程池來管理這些異步任務么?

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個線程,再提交的任務直接丟棄

sch06.gif

簡單說一下,用自定義線程池的好處:

  • 合理的分配線程池參數
  • 拒絕策略的選擇也比較有意思(可以按照自己的想法來處理"負載"的任務)
  • 線程池命名,對於以后問題排查,會有很大的幫助

6. 小結

本來這篇博文在昨天即8月2號就應該寫完的,結果晚上生產環境下除了點問題,解決線上故障之后就比較晚了,留到了今天,哎,拖延症也是要不得。。。

下面小結Spring中定時任務的幾個知識點

  • 默認所有的定時任務都是串行調度的,一個線程,且即便crond完全相同的兩個任務先后順序也沒法保證(具體原因需要源碼分析,看下這塊是怎么支持)
  • 使用@Async注解可以使定時任務異步調度;但是需要開啟配置,在啟動類上添加 @EnableAsync 注解
  • 開啟並發執行時,推薦用自定義的線程池來替代默認的,理由見上面

II. 其他

0. 相關

1. 一灰灰Bloghttps://liuyueyi.github.io/hexblog

一灰灰的個人博客,記錄所有學習和工作中的博文,歡迎大家前去逛逛

2. 聲明

盡信書則不如,已上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現bug或者有更好的建議,歡迎批評指正,不吝感激

3. 掃描關注

小灰灰Blog&公眾號

QrCode

知識星球

zhishi


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM