現代的應用程序早已不是之前的那些由簡單的增刪改查拼湊而成的程序了,高複雜性早已經是標配,而任務的定時調度與執行也是對程序的基本要求了。java
不少業務需求的實現都離不開定時任務,例如,每個月一號,移動將清空你上月未用完流量,重置套餐流量,以及備忘錄提醒、鬧鐘等功能。git
Java 系統中主要有三種方式來實現定時任務:程序員
下面咱們一個個來看。github
先看一個小 demo,接着咱們再來分析其中原理:數據庫
這種方式的定時任務主要用到兩個類,Timer 和 TimerTask。其中,TimerTask 繼承接口 Runnable,抽象的描述一種任務類型,咱們只要重寫實現它的 run 方法就能夠實現自定義任務。數組
而 Timer 就是用於定時任務調度的核心類,demo 中咱們調用其 schedule 並指定延時 1000 毫秒,因此上述代碼會在一秒鐘後完成打印操做,接着程序結束。安全
那麼,使用上很簡單,兩個步驟便可,可是其中的實現邏輯是怎樣的呢?微信
Timer 接口框架
首先,Timer 接口中,這兩個字段是很是核心重要的:異步
TaskQueue 是一個隊列,內部由動態數組實現的最小堆結構,換句話說,它是一個優先級隊列。而優先級參考下一次執行時間,越快執行的越排在前面,這一點咱們回頭再研究。
接着,這個 TimerThread 類實際上是 Timer 的一個內部類,它繼承了 Thread 並重寫了其 run 方法,該線程實例將在構建 Timer 實例的時候被啓動。
run 方法內部會循環的從隊列中取任務,若是沒有就阻塞本身,而當咱們成功的向隊列中添加了定時任務,也會嘗試喚醒該線程。
咱們也來看一下 Timer 的構造方法:
public Timer(String name) { thread.setName(name); thread.start(); }
再簡單不過的構造函數了,爲內部線程設置線程名,並啓動該線程。
最後,咱們着重看一下 Timer 中用於配置一個定時任務進任務隊列的方法。
//在時刻 time 處執行任務 schedule(TimerTask task, Date time) //延時 delay 毫秒後執行任務 schedule(TimerTask task, long delay) //固定延時重複執行,firstTime爲首次執行時間, //日後沒間隔 period 毫秒執行一次 schedule(TimerTask task, Date firstTime, long period) //固定延時重複執行 //首次執行時間爲當前時間延時 delay 毫秒 schedule(TimerTask task, long delay, long period) //固定頻率重複執行,每過 period 毫秒執行一次 scheduleAtFixedRate(TimerTask task, Date firstTime, long period) //固定頻率重複執行 scheduleAtFixedRate(TimerTask task, long delay, long period)
相信有了註釋,這幾個方法的區別與做用應該不難理解,可是其中有兩個概念須要做一點區分。
==固定延時== VS ==固定頻率==
固定延時:以任務的上一次 實際 執行時間作參考,日後延時 period 毫秒。
固定頻率:任務的日後每一次執行時間都在任務提交的那一刻獲得了肯定,不論你上次任務是否意外延時了,定時定點執行下一次任務。
這二者的區別仍是很大的,但願你可以理解清楚,接着咱們以其中一個方法爲例,看看底層實現。
以這個方法爲例,其餘重載方法的底層調用都是一樣的,咱們不去贅述。
這個方法的做用,咱們再說一遍。
以當前時間爲準,延時 delay 毫秒後第一次執行該任務,而且採起固定延時的方式,每隔 period 毫秒再次執行該任務。
開頭的兩個異常判斷咱們再也不贅述,看看 sched 方法:
方法須要傳入三個參數,參數 task 表明的須要執行的任務體,TimerTask 咱們回頭會詳細介紹,這裏你知道它表明了一個任務體便可。
參數 time 描述了該任務下一次執行的時刻,計算機底層是以毫秒描述時刻的,因此這裏轉換爲 long 類型來描述時刻。
參數 period 是固定延時的毫秒數。
整個方法的邏輯咱們能夠總結歸納一下,具體的代碼就不一行行分析了,由於也不難。
可能會有人疑問,Timer 如何判斷一個任務是不是重複執行的,仍是單次執行就結束的?
答案在 TimerThread 的 run 方法裏,有興趣你能夠去研究下,方法體比較多比較長,這裏不作分析。
當咱們構造 Timer 實例的時候,就會啓動該線程,該線程會在一個死循環中嘗試從任務隊列上獲取任務,若是成功獲取就執行該任務並在執行結束以後作一個判斷。
若是 period 值爲零,則說明這是一次普通任務,執行結束後將從隊列首部移除該任務。
若是 period 爲負值,則說明這是一次固定延時的任務,修改它下次執行時間 nextExecutionTime 爲當前時間減去 period,重構任務隊列。
若是 period 爲正數,則說明這是一次固定頻率的任務,修改它下次執行時間爲 上次執行時間加上 period,並重構任務隊列。
其實,我也已經把 TimerThread 的 run 方法裏最核心的邏輯也已經介紹了,建議你們親自去研究研究具體代碼的實現,你會對這一塊的邏輯更清晰。
最後,咱們看一看這個 Timer 它有哪些劣勢的地方:
因此你看,單線程的 Timer 帶來了太多侷限性,因而咱們看它的替代者。
PS:原本計劃再介紹下 TimerTask 這個抽象任務類的,可是發現實在沒啥好介紹的,就是增長了兩個字段,一個用於記錄下一次該任務的執行時間,一個用於延時毫秒數。你也只須要重寫其 run 方法便可。
這個接口相信你必定眼熟,我告訴你在哪見過。
你看,它是咱們異步框架中的接口,正好咱們今天來介紹他,這樣整個異步框架中全部的接口咱們都分析過了。
ScheduledExecutorService中定義的這四個接口方法和 Timer 中對應的方法幾乎同樣,只不過 Timer 的 scheduled 方法須要在外部傳入一個 TimerTask 的抽象任務。
而咱們的 ScheduledExecutorService 封裝的更加細緻了,隨便你傳 Runnable 或是 Callable,我會在內部給你作一層封裝,封裝一個相似 TimerTask 的抽象任務類(ScheduledFutureTask)。
而後傳入線程池,啓動線程去執行該任務,而咱們的 ScheduledFutureTask 重寫的 run 方法是這樣的:
若是 periodic 爲 true 則說明這是一個須要重複執行的任務,不然說明是一個一次性任務。
因此實際執行該任務的時候,須要分類,若是是普通的任務就直接調用 run 方法執行便可,不然在執行結束以後還須要重置下下一次執行時間。
總體來講,ScheduledExecutorService 區別於 Timer 的地方就在於前者依賴了線程池來執行任務,而任務自己會判斷是什麼類型的任務,須要重複執行的在任務執行結束後會被從新添加到任務隊列。
而對於後者來講,它只依賴一個線程不停的去獲取隊列首部的任務並嘗試執行它,不管是效率上、仍是安全性上都比不上前者。
因此,建議使用 ScheduledExecutorService 取代 Timer,固然,經過學習 Timer 會更有助於對 ScheduledExecutorService 的研究。
除了上述兩種定時任務框架外,Java 生態圈還存在一種開源的三方框架,他就是 Quartz。
Quartz 是一個功能完善的任務調度框架,支持集羣環境下的任務調度,須要將任務調度狀態序列化到數據庫。
Quartz 已是隨着分佈式概念的流行,成爲企業級定時任務調度框架中的不二選擇。
Quartz 這個框架的使用及與原理在本篇就不作介紹了,咱們會在後續介紹分佈式概念的時候再來介紹它與 SpringCloud 平臺下的整合使用狀況。