定時任務方案大百科

原文地址:https://crossoverjie.top

前言

節前有更新一篇定時任務的相關文章《延時消息之時間輪》,有朋友提出希望可以完整的介紹下常見的定時任務方案,于是便有了這篇文章。

Timer

本次會主要討論大家使用較多的方案,首先第一個就是 Timer 定時器,它可以在指定時間后運行或周期性運行任務;使用方法也非常簡單:

這樣便可創建兩個簡單的定時任務,分別在 3s/5s 之后運行。

使用起來確實很簡單,但也有不少毛病,想要搞清楚它所存在的問題首先就要理解其實現原理。

實現原理

定時任務要想做到按照我們給定的時間進行調度,那就得需要一個可以排序的容器來存放這些任務。

Timer 中內置了一個 TaskQueue 隊列,用于存放所有的定時任務。

其實本質上是用數組來實現的一個最小堆,它可以讓每次寫入的定時任務都按照執行時間進行排序,保證在堆頂的任務執行時間是最小的。

這樣在需要執行任務時,每次只需要取出堆頂的任務運行即可,所以它取出任務的效率很高為

。

結合代碼會比較容易理解:

在寫入任務的時候會將一些基本屬性存放起來(任務的調度時間、周期、初始化任務狀態等),最后就是要將任務寫入這個內置隊列中。


在任務寫入過程中最核心的方法便是這個 fixUp() ,它會將寫入的任務從隊列的中部通過執行時間與前一個任務做比對,一直不斷的向前比較。

如果這個時間是最早執行的,那最后將會被移動到堆頂。

通過這個過程可以看出 Timer 新增一個任務的時間復雜度為

。


再來看看它執行任務的過程,其實在初始化 Timer 的時候它就會在后臺啟動一個線程用于從 TaskQueue 隊列中獲取任務進行調度。


所以我們只需要看他的 run() 即可。

從這段代碼中很明顯可以看出這個線程是一直不斷的在調用

task = queue.getMin();

來獲取任務,最后使用 task.run() 來執行任務。

getMin() 方法中可以看出和我們之前說的一致,每次都是取出堆頂的任務執行。

一旦取出來的任務執行時間滿足要求便可運行,同時需要將它從這個最小堆實現的隊列中刪除;也就是調用的 queue.removeMin() 方法。


其實它的核心原理和寫入任務類似,只不過是把堆尾的任務提到堆頂,然后再依次比較將任務往后移,直到到達合適的位置。

從剛才的寫入和刪除任務的過程中其實也能看出,這個最小堆只是相對有序并不是絕對的有序。

源碼看完了,自然也能得出它所存在的問題了。

  • 后臺調度任務的線程只有一個,所以導致任務是阻塞運行的,一旦其中一個任務執行周期過長將會影響到其他任務。
  • Timer 本身沒有捕獲其他異常(只捕獲了 InterruptedException),一旦任務出現異常(比如空指針)將導致后續任務不會被執行。

ScheduledExecutor

既然 Timer 存在一些問題,于是在 JDK1.5 中的并發包中推出了 ScheduledThreadPoolExecutor 來替代 Timer,從它所在包路徑也能看出它本身是支持任務并發執行的。

先來看看它的類繼承圖:

可以看到他本身也是一個線程池,繼承了 ThreadPoolExecutor。

從他的構造函數中也能看出,本質上也是創建了一個線程池,只是這個線程池中的阻塞隊列是一個自定義的延遲隊列 DelayedWorkQueue(與 Timer 中的 TaskQueue 作用一致)


新建任務

當我們寫入一個定時任務時,首先會將任務寫入到 DelayedWorkQueue 中,其實這個隊列本質上也是使用數組實現的最小堆。

新建任務時最終會調用到 offer() 方法,在這里也會使用 siftUp() 將寫入的任務移動到堆頂。

原理就和之前的 Timer 類似,只不過這里是通過自定義比較器來排序的,很明顯它是通過任務的執行時間進行比較的。

運行任務

所以這樣就能將任務按照執行時間的順序排好放入到線程池中的阻塞隊列中。

這時就得需要回顧一下之前線程池的知識點了:

在線程池中會利用初始化時候的后臺線程從阻塞隊列中獲取任務,只不過在這里這個阻塞隊列變為了 DelayedWorkQueue,所以每次取出來的一定是按照執行時間排序在前的任務。


Timer 類似,要在任務取出后調用 finishPoll() 進行刪除,也是將最后一個任務提到堆頂,然后挨個對比移動到合適的位置。

而觸發消費這個 DelayedWorkQueue 隊列的地方則是在寫入任務的時候。


本質上是調用 ThreadPoolExecutoraddWorker() 來寫入任務的,所以消費 DelayedWorkQueue 也是在其中觸發的。

這里更多的是關于線程池的知識點,不太清楚的可以先看看之前總結的線程池篇,這里就不再贅述。

原理看完了想必也知道和 Timer 的優勢在哪兒了。

Timer ScheduledThreadPoolExecutor
單線程阻塞 多線程任務互不影響
異常時任務停止 依賴于線程池,單個任務出現異常不影響其他任務

所以有定時任務的需求時很明顯應當淘汰 Timer 了。

時間輪

最后一個是基于時間輪的定時任務,這個我在上一篇《延時消息之時間輪》有過詳細介紹。

通過源碼分析我們也可以來做一個對比:

ScheduledThreadPoolExecutor 基于時間輪
寫入效率
基于最小堆,任務越多效率越低
HashMap 的寫入類似,效率很高。
執行效率
每次取出第一個,效率很高
每秒撥動一個指針取出任務

所以當寫入的任務較多時,推薦使用時間輪,它的寫入效率更高。

但任務很少時其實 ScheduledThreadPoolExecutor 也不錯,畢竟它不會每秒都去撥動指針消耗 CPU ,而是一旦沒有任務線程會阻塞直到有新的任務寫入進來。

RingBufferWheel 更新

在之前的《延時消息之時間輪》中自定義了一個基于時間輪的定時任務工具 RingBufferWheel ,在網友的建議下這次順便也做了一些調整,優化了 API 也新增了取消任務的 API。

在之前的 API 中,每當新增一個任務都要調用一下 start(),感覺很怪異;這次直接將啟動函數合并到 addTask 中,使用起來更加合理。

同時任務的寫入也支持并發了。


不過這里需要注意的是 start() 在并發執行的時候只能執行一次,于是就利用了 CAS 來保證同時只有一個線程可以執行成功。

同時在新增任務的時候會返回一個 taskId ,利用此 ID 便可實現取消任務的需求(雖然是比較少見),使用方法如下:

感興趣的朋友可以看下源碼也很容易理解。

分布式定時任務

最后再擴展一下,上文我們所提到的所有方案都是單機版的,只能在單個進程中使用。

一旦我們需要在分布式場景下實現定時任務的高可用、可維護之類的需求就得需要一個完善的分布式調度平臺的支持。

目前市面上流行的開源解決方案也不少:

我個人在工作中只使用過前面兩者,都能很好的解決分布式調度的需求;比如高可用、統一管理、日志報警等。

當然這些開源工具其實在定時調度這個功能上和上文中所提到的一些方案是分不開的,只是需要結合一些分布式相關的知識;比遠程調用、統一協調、分布式鎖、負載均衡之類的。

感興趣的朋友可以自行查看下他們的源碼或官方文檔。

總結

一個小小的定時器其實涉及到的知識點還不少,包括數據結構、多線程等,希望大家看完多少有些幫助,順便幫忙點贊轉發搞起??。

本文所涉及到的所有源碼:

https://github.com/crossoverJie/cim

你的點贊與分享是對我最大的支持

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容