在介紹之前,還是經(jīng)典的幾個問題:
1、Timer是什么?能干什么?
2、Timer的使用案例?
3、Timer的原理?
4、Timer教其他同類工具的優(yōu)缺點?
????1、Timer是jdk中提供的一個定時器工具,使用的時候會在主線程之外起一個單獨的線程執(zhí)行指定的計劃任務(wù),可以指定執(zhí)行一次或者反復(fù)執(zhí)行多次。
????2、Timer的使用,這個先來一個簡單的demo。

結(jié)果如下:

????3、要介紹原理,得先從源碼入手,看下Timer的方法如下:

????其中我們要重點介紹schedule方法和scheduleAtFixedRate方法,這個是Timer能實現(xiàn)定時任務(wù)的核心。
????還有我們要介紹Timer類的兩個內(nèi)部類:
????1、TaskQueue,TaskQueue是一個隊列,看下里面的內(nèi)容。

????其中存儲的是TimerTask類,上面demo里面的PrintTask就是TimerTask的之類,最終也是會進(jìn)入這個隊列里面的。
????看下add(TimerTask task)方法代碼。

????如果長度超過隊列的長度,就把隊列擴(kuò)展,生成一個新隊列賦值給queue變量。
fixUp(size)是關(guān)鍵代碼,看下到底做了什么?

????其中nextExecutionTime為TimerTask的字段,表示下一次執(zhí)行的時間戳。
????所以由上面代碼可以知道,目的是為了進(jìn)行排序,先不管是什么排序(好像是二元選擇,由于今天說的不是排序,大家可以去看下),目的是把剛剛添加的這個時間任務(wù)根據(jù)他的nextExecutionTime放到合適的位置。按照下標(biāo)的順序從小到大排序。
????2、接下來介紹另外一個內(nèi)部類TimerThread,看類名就可以知道是個線程類。

看下類的內(nèi)部結(jié)構(gòu):
一個構(gòu)造方法,參數(shù)是TaskQueue(TimerThread(TaskQueue))
一個newTasksMayBeScheduled的布爾類型成員變量,用來標(biāo)識是否有可能有新任務(wù)被安排。
一個私有的方法mainLoop()這個是方法是核心。
一個run方法,每個Thread都會復(fù)寫的。
先看下run方法的代碼:

由此可知,其中調(diào)用了mainLoop()方法,finally塊中表名,線程停止是否,會清掉隊列,設(shè)置newTasksMayBeScheduled為false;
接下來看mainLoop方法:

以上代碼是Timer實現(xiàn)的關(guān)鍵,注意前提是queue成員函數(shù)已經(jīng)是按照執(zhí)行時間排好序的(上面已經(jīng)在fixUp中介紹過了),先不考慮周期等情況下(period=0未非周期性執(zhí)行),我們再解讀一下源碼:
????1.518行執(zhí)行的mainLoop函數(shù),顧名思義,就是主要的循環(huán)函數(shù),里面有個死循環(huán)。
????2.523行有個鎖,鎖住的對象是任務(wù)隊列queue。
????3.525行代表是隊列為空且有可能有新的任務(wù)被安排時,會執(zhí)行queue.wait()函數(shù),線程進(jìn)入wait狀態(tài)(讓出對臨界資源的占用權(quán)),等待被notify。(等待生產(chǎn)者生產(chǎn)消息)。
????4.532行把當(dāng)前隊列中最小的任務(wù)賦值給task變量。
????5.540行是未taskFired賦值,且判斷是否要執(zhí)行,taskFired是任務(wù)的下一次執(zhí)行時間和當(dāng)前時間的比較,如果<=當(dāng)前時間則為true,反之false。
????6.541行代表的是如果是非周期性執(zhí)行的話,這刪除當(dāng)前隊列中最小的那個。
????從532行可以看到就是當(dāng)前進(jìn)行比較的task,而且在removeMin()方法中也會進(jìn)行一次排序,這邊就不再介紹。
????7.551行的代碼是說如果當(dāng)前的任務(wù)執(zhí)行實際未到taskFired==false,就會執(zhí)行queue.wait(timeout)函數(shù),其中的timeout就是超時時間,到了超時時間代碼會自動喚醒,重新獲取鎖。
????8.554行的代碼可以看到,如果任務(wù)已經(jīng)可以執(zhí)行了,就會指教調(diào)用task.run(),這邊有個疑問?就是既然是個task,且這個TimerTaskimplementsRunnable ,應(yīng)該是按照線程的方式啟動,應(yīng)該newThread(task).start。這邊為什么只是單純的調(diào)用run方法而已,會導(dǎo)致什么問題,后面會介紹。
????9.此處代碼的解釋是:用一個進(jìn)程里面的死循環(huán)來監(jiān)控隊列(已經(jīng)排完序了),但是又不能一直輪詢下去,這樣很耗CPU,所以設(shè)計者就用了生產(chǎn)消費者模式,使用Object的wait/notify這類特性,進(jìn)行及時通知。讓線程及時被喚醒,這個線程起來跑起任務(wù),就會非常及時執(zhí)行任務(wù)。(正常情況下,如果這個時候機器有其他大型運算在進(jìn)行,可能線程就會有稍微一點延遲喚醒,這個基本上可以忽略不計)
????Object.wait(long timeout)與Object.wait()方法不一樣,雖然都是可以讓對象掛起,但是wait(long timeout)超時會自動喚醒,而wait()則只能等待被notify(),notifyAll()方法喚醒,否則會一直沉睡下去。
現(xiàn)在原理已經(jīng)知道了,但還是個疑問就是第3點,如果線程進(jìn)入wait了(消費者消費等待),誰來喚醒他(生產(chǎn)者生產(chǎn)消息)?
看以下代碼:

這個方法是所有要添加到隊列里面的任務(wù)的最終方法,他的上層代碼會抽成不重復(fù)執(zhí)行,period=0,時間為Date和delay ,最終time=Date.getTime()或者System.currentTimeMillis()+delay等。
咱們此處還是只針對非周期性的任務(wù)進(jìn)行分析。
Period=0。
以上的代碼可以看到有兩個synchronized鎖,synchronized(queue):395~411行,synchronized(task.lock):399~406行。
以上的代碼主要做了幾件事如下:
????1.line395,一次只允許一個線程操作queue對象.
????2.Line403~line405可知,這里面把一些所需要的重要屬性都賦值給task
????3.line408添加任務(wù)進(jìn)入隊列中,這個是最重要的,在上文中寫道這個方法會對task進(jìn)行排序。
????4.line409表示當(dāng)queue.getMin()==task,內(nèi)部就是添加到隊列之后,queue[1]==task則執(zhí)行queue.notify(),喚醒mainLoop方法里面queue.wait方法,這個時候就可以進(jìn)行執(zhí)行。
至于這邊有個奇怪的地方,就是queue的最小是任務(wù)是queue[1],其實是TaskQueue這個內(nèi)部類的設(shè)計(這個地方需要討論一下為什么要這么設(shè)計),她直接跳過task[0],從以下get(i)的注釋中可以看出來,隊列的頭在數(shù)組中的下標(biāo)是1.

以上我們分析了任務(wù)的根本設(shè)計,就是任務(wù)如何做到定時啟動的。
以上的設(shè)計,其中的基本流程圖如下:

現(xiàn)在我們要分析其中未分析到周期執(zhí)行任務(wù)。
周期執(zhí)行任務(wù)主要有兩種方法:
1、

2、

看著兩個方法的代碼,除了方法名和參數(shù)校驗外,基本上差別不大,如果去掉忽略掉隊period的處理,完全就是同一個方法。
我們知道這兩個方法都有同一個特性,就是可以對任務(wù)進(jìn)行周期性執(zhí)行,上Demo。

在上面的demo上做了些修改,結(jié)果如下:

可以看到,這兩個方法的允許結(jié)果沒有差別。
其實這兩個方法是有區(qū)別的,可以看出來scheduleAtFixedRate的方法名注釋就是在第一次執(zhí)行時間早于當(dāng)前時間時,她會進(jìn)行補充,這個可以通過實驗說明,我們現(xiàn)在把第一次執(zhí)行的時間在當(dāng)前時間之前30S執(zhí)行,10S執(zhí)行一次。

執(zhí)行結(jié)果如下:

從結(jié)果可以看出來,D任務(wù)在10:47:34的時候,除了和C一樣在第一次執(zhí)行的時候都會執(zhí)行之外,她有執(zhí)行了3次,剛好補充上“缺失”30S時間。
所以scheduleAtFixedRate方法具有“補充性”,一種翻譯叫做“追趕性”。
接下來還是解讀一下源碼吧:
從上文中我們知道,period參數(shù)在校驗過去后,直接賦值給Task的period。
由于上面已經(jīng)有了mainLoop的全部代碼,我這邊就截取最關(guān)鍵的代碼進(jìn)行說明:

前面介紹了,541行的if(period==0)表示非周期性執(zhí)行,則從隊列中去掉這個任務(wù),并且設(shè)置任務(wù)的狀態(tài)為執(zhí)行完畢。
else是周期性執(zhí)行,最關(guān)鍵的代碼是兩部分:
1)rescheduleMin,表示的是設(shè)置一個新時間給當(dāng)前隊列中head任務(wù)queue[1],其實就是當(dāng)前的任務(wù)了,后面會進(jìn)行finxDown(1)的排序。所以當(dāng)前的任務(wù)就變成一個新任務(wù)加入到隊列中。

1)三目表達(dá)式:task.period<0? currentTime- task.period
: executionTime + task.period
小于0非補充性的重復(fù)任務(wù),這新時間為當(dāng)前時間-task.period,由于前文可知非補充性的任務(wù)period為設(shè)置值得負(fù)值,所以假設(shè)我們要10秒鐘跑一次,這邊相當(dāng)于currentTime+10S解決。
大于0表示補充性的重復(fù)任務(wù),還是假設(shè)10S跑一次,這邊是executionTime+10S所以也正常。
最后怎么樣解釋他的補充性,用上面demo里面的任務(wù)來理解吧:
現(xiàn)在是Mon Dec 11 10:47:34 CST 2017
[C]的打印時間為:Mon Dec 11 10:47:34 CST 2017
[D]的打印時間為:Mon Dec 11 10:47:34 CST 2017
[D]的打印時間為:Mon Dec 11 10:47:34 CST 2017
[D]的打印時間為:Mon Dec 11 10:47:34 CST 2017
[D]的打印時間為:Mon Dec 11 10:47:34 CST 2017
[D]的打印時間為:Mon Dec 11 10:47:44 CST 2017
[C]的打印時間為:Mon Dec 11 10:47:44 CST 2017
[D]的打印時間為:Mon Dec 11 10:47:54 CST 2017
[C]的打印時間為:Mon Dec 11 10:47:54 CST 2017
[D]的打印時間為:Mon Dec 11 10:48:04 CST 2017
[C]的打印時間為:Mon Dec 11 10:48:04 CST 2017
[D]的打印時間為:Mon Dec 11 10:48:14 CST 2017
[C]的打印時間為:Mon Dec 11 10:48:14 CST 2017
粗體字體為補充性的任務(wù)。
去除年月日等等信息:(舉例分析)
currentTime=10:47:34
executionTime=10:47:34-30*1000=10:47:14
對于C來說:的newTime=currentTime+ 10*1000=10:47:44
對于D來說:的newTime= executionTime + 10*1000=10:47:24(如此重復(fù)3次,才能追趕上C的currentTime),所以這邊的追趕性就是這個原理。
上面還有個問題,還沒說明,就是為何調(diào)用run,這樣不就并行了么,是否會導(dǎo)致如果上面的任務(wù)如果執(zhí)行時間太長,影響下面任務(wù)的執(zhí)行。
這個是事實,大家可以做實驗,這邊明顯就是串行執(zhí)行的,雖然TimerTask是一個線程類,但是最終沒有以線程的方式啟動它,這就導(dǎo)致他的時效性有時候難以保證,還有就是如果其中某個任務(wù)異常了,這個時候異常是直接拋到啟動它的主線程里面,導(dǎo)致所有任務(wù)都停止了,這個可以從源碼中可以看出,while里面也沒有針對異常進(jìn)行處理。
這個設(shè)計者最初一定是有原因的,看業(yè)務(wù)來做吧,如果有可能出現(xiàn)時間過長的任務(wù)需要處理,且后面的任務(wù)對實時性要求教高,就建議用別的工具。
優(yōu)點:單線程,省線程資源,且使用方便。
缺點:各個任務(wù)之間可能會造成互相影響。Timer當(dāng)任務(wù)拋出異常時的缺陷,如果TimerTask拋出RuntimeException,Timer會停止所有任務(wù)的運行。
以下是簡單流程圖:
接下來簡單介紹其他的幾種任務(wù)調(diào)度器:
ScheduledThreadPoolExecutor
這個也是jdk帶的一個任務(wù)調(diào)度器。

是從jdk1.5開始進(jìn)入并發(fā)工具包,作者是Doug Lea大神。
這邊改為一個任務(wù)一個線程。
優(yōu)點:修復(fù)Timer上面的各個任務(wù)之間互相影響的問題。
缺點:耗費太多線程了,很容易造成OOM,而且功能較少,上面的Timer也一樣。
以上兩個jdk自帶的工具類,都有一些缺陷,Timer和ScheduledExecutor都僅能提供基于開始時間與重復(fù)間隔的任務(wù)調(diào)度,不能勝任更加復(fù)雜的調(diào)度需求。比如,設(shè)置每星期二的16:38:10執(zhí)行任務(wù)。該功能使用Timer和ScheduledExecutor都不能直接實現(xiàn),但我們可以借助Calendar間接實現(xiàn)該功能。
開源工具包Quartz
Quartz就能解決以上痛點,看下介紹:
Quartz是個開源的作業(yè)調(diào)度框架,為在Java應(yīng)用程序中進(jìn)行作業(yè)調(diào)度提供了簡單卻強大的機制。Quartz框架包含了調(diào)度器監(jiān)聽、作業(yè)和觸發(fā)器監(jiān)聽。你可以配置作業(yè)和觸發(fā)器監(jiān)聽為全局監(jiān)聽或者是特定于作業(yè)和觸發(fā)器的監(jiān)聽。Quartz允許開發(fā)人員根據(jù)時間間隔(或天)來調(diào)度作業(yè)。它實現(xiàn)了作業(yè)和觸發(fā)器的多對多關(guān)系,還能把多個作業(yè)與不同的觸發(fā)器關(guān)聯(lián)。整合了Quartz的應(yīng)用程序可以重用來自不同事件的作業(yè),還可以為一個事件組合多個作業(yè)。并且還能和Spring配置整合使用。
缺點還是線程問題過多咯。