天天看點

深入 Java Timer 定時任務排程器實作原理Timer 使用内部結構 任務狀态任務鎖 任務隊列空了Timer 終止 垃圾回收

使用 Java 來排程定時任務時,我們經常會使用 Timer 類搞定。Timer 簡單易用,其源碼閱讀起來也非常清晰,本節我們來仔細分析一下 Timer 類,來看看 JDK 源碼的編寫者是如何實作一個穩定可靠的簡單排程器。

Timer 使用

Timer 排程任務有一次性排程和循環排程,循環排程有分為固定速率排程(fixRate)和固定時延排程(fixDelay)。固定速率就好比你今天加班到很晚,但是到了第二天還必須準點到公司上班,如果你一不小心加班到了第二天早上 9 點,你就連休息的時間都沒有了。而固定時延的意思是你必須睡夠 8 個小時再過來上班,如果你加班到淩晨 6 點,那就可以下午過來上班了。固定速率強調準點,固定時延強調間隔。

如果你有一個任務必須每天準點排程,那就應該使用固定速率排程,并且要確定每個任務執行時間不要太長,千萬别超過了第二天這個點。如果你有一個任務需要每隔幾分鐘跑一次,那就使用固定時延排程,它不是很在乎你的單個任務要跑多長時間。

内部結構

Timer 類裡包含一個任務隊列和一個異步輪訓線程。任務隊列裡容納了所有待執行的任務,所有的任務将會在這一個異步線程裡執行,切記任務的執行代碼不可以抛出異常,否則會導緻 Timer 線程挂掉,所有的任務都沒得執行了。單個任務也不易執行時間太長,否則會影響任務排程在時間上的精準性。比如你一個任務跑了太久,其它等着排程的任務就一直處于饑餓狀态得不到排程。所有任務的執行都是這單一的 TimerThread 線程。

堆排序

Timer 的任務隊列 TaskQueue 是一個特殊的隊列,它内部是一個數組。這個數組會按照待執行時間進行堆排序,堆頂元素總是待執行時間最小的任務。輪訓線程會每次輪訓出時間點最近的并且到點的任務來執行。數組會自動擴容,如果任務非常多。

任意線程都可以通過 Timer.schedule 方法将任務加入 TaskQueue,但是 TaskQueue 又并不是線程安全的資料結構。所在每次修改 TaskQueue 時都需要加鎖。

任務狀态

TimerTask 有 4 個狀态,VIRGIN 是預設狀态,剛剛執行個體化還沒有被排程。SCHEDULED 表示已經将任務塞進 TaskQueue 等待被執行。EXECUTED 表示任務已經執行完成。CANCELLED 表示任務被取消了,還沒來得及執行就被人為取消了。

對于一個循環任務來說,它不存在 EXECUTED 狀态,因為它每次剛剛執行完成,就被重新排程了。EXECUTED 狀态僅僅存在于一次性任務,而且這個狀态其實并不是表示任務已經執行完成,它是指已經從任務隊列裡摘出來了,馬上就要執行。

任務間隔字段 period 比較特殊,當使用固定速率時,period 為正值,當使用固定間隔時,period 為負值,當任務是一次性時,period 為零。下面是循環任務的下次排程時間設定

對于固定速率來說,如果任務執行時間太長超出了間隔,那麼它可能會持續霸占任務隊列,因為它的排程時間将總是低于 currentTime,排在堆頂,每次輪訓取出來的都是它。運作完畢後,重新排程這個任務,它的時間依舊趕不上。持續下去你會看到這個任務的排程時間遠遠落後于目前時間,而其它任務可能會徹底餓死。這就是為什麼一定要特别注意固定速率的循環任務運作時間不宜過長。

任務鎖

Timer 的任務支援取消操作,取消任務的線程和執行任務的線程極有可能不是一個線程。有可能任務正在執行中,結果另一個線程表示要取消任務。這時候 Timer 是如何處理的呢?在 TimerTask 類裡看到了一把鎖。當任務屬性需要修改的時候,都會加鎖。

在任務運作之前會檢查任務是不是已經被取消了,如果取消了,就從隊列中移除。一旦任務開始運作 run(),對于單次任務來說它就無法被取消了,而循環任務将不會繼續下次排程。如果任務沒有機會得到執行(時間設定的太長),那麼即使這個任務被取消了,它也會一直持續躺在任務隊列中。設想如果你排程了一系列久遠的任務,然後都取消了,這可能會成為一個記憶體洩露點。是以 Timer 還單獨提供了一個 purge() 方法可以一次性清空所有的已取消的任務。

任務隊列空了

任務隊列裡沒有任務了,排程線程必須按一定的政策進行睡眠。它需要睡眠一直到最先執行的任務到點時立即醒來,是以睡眠截止時間就是第一個任務将要執行的時間。同時在睡覺的時候,有可能會有新的任務被添加進來,它的排程時間可能會更加提前,是以當有新的任務到來時需要可以喚醒正在睡眠的線程。

代碼中的 wait() 方法就是調用了 Object.wait() 來進行睡眠。當有新任務進來了,發現這個新任務的運作時間是最早的,那就調用 notify() 方法喚醒輪訓線程。

Timer 終止

Timer 提供了 cancel() 方法清空隊列,停止排程器,不允許有任何新任務進來。它會将 newTasksMayBeScheduled 字段設定為 false 表示 Timer 即将終止。

如果 Timer 終止了,還有新任務進來就會抛出異常。

我們還注意到 Timer.cancel() 方法會喚醒輪訓線程,為的是可以立即停止輪訓。不過如果任務正在執行中,這之後 cancel() 就必須等到任務執行完畢才可以停止。

垃圾回收

還有一個特殊的場景需要特别注意,那就是當輪訓線程因為隊列裡沒有任務而睡眠的時候,Timer 對象因為不再被引用而被垃圾回收了。這時候需要主動喚醒輪訓線程,讓它退出。

當 Timer 被回收時,内部字段 threadPeaper 指向的對象也會被回收。是以 finalize 方法将會被調用,喚醒并終止 Timer 輪訓線程。如果沒有這個 threadPeaper 對象就可能會導緻 JVM 裡留下僵屍線程。

歡迎工作一到五年的Java工程師朋友們加入Java填坑之路:860113481

群内提供免費的Java架構學習資料(裡面有高可用、高并發、高性能及分布式、Jvm性能調優、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!