Timer時間控制器的源碼解析

在介紹之前,還是經(jīng)典的幾個問題:

1、Timer是什么?能干什么?

2、Timer的使用案例?

3、Timer的原理?

4、Timer教其他同類工具的優(yōu)缺點?

????1、Timer是jdk中提供的一個定時器工具,使用的時候會在主線程之外起一個單獨的線程執(zhí)行指定的計劃任務(wù),可以指定執(zhí)行一次或者反復(fù)執(zhí)行多次。

????2、Timer的使用,這個先來一個簡單的demo。

基本的Demo

結(jié)果如下:

demo運行結(jié)果

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

Timer內(nèi)部的方法

????其中我們要重點介紹schedule方法和scheduleAtFixedRate方法,這個是Timer能實現(xiàn)定時任務(wù)的核心。

????還有我們要介紹Timer類的兩個內(nèi)部類:

????1、TaskQueue,TaskQueue是一個隊列,看下里面的內(nèi)容。

TaskQueue的方法和成員變量

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

????看下add(TimerTask task)方法代碼。

add(TimerTask task)方法代碼

????如果長度超過隊列的長度,就把隊列擴(kuò)展,生成一個新隊列賦值給queue變量。

fixUp(size)是關(guān)鍵代碼,看下到底做了什么?

fixUp(size)代碼邏輯,對后面有參考作用

????其中nextExecutionTime為TimerTask的字段,表示下一次執(zhí)行的時間戳。

????所以由上面代碼可以知道,目的是為了進(jìn)行排序,先不管是什么排序(好像是二元選擇,由于今天說的不是排序,大家可以去看下),目的是把剛剛添加的這個時間任務(wù)根據(jù)他的nextExecutionTime放到合適的位置。按照下標(biāo)的順序從小到大排序。

????2、接下來介紹另外一個內(nèi)部類TimerThread,看類名就可以知道是個線程類。

TimerThread內(nèi)部結(jié)構(gòu)

看下類的內(nèi)部結(jié)構(gòu):

一個構(gòu)造方法,參數(shù)是TaskQueue(TimerThread(TaskQueue))

一個newTasksMayBeScheduled的布爾類型成員變量,用來標(biāo)識是否有可能有新任務(wù)被安排。

一個私有的方法mainLoop()這個是方法是核心。

一個run方法,每個Thread都會復(fù)寫的。

先看下run方法的代碼:

線程的run方法

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

接下來看mainLoop方法:

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)消費者模式,使用Objectwait/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)消息)?

看以下代碼:

sched方法

這個方法是所有要添加到隊列里面的任務(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.

get方法

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

以上的設(shè)計,其中的基本流程圖如下:


流程圖

現(xiàn)在我們要分析其中未分析到周期執(zhí)行任務(wù)。

周期執(zhí)行任務(wù)主要有兩種方法:

1、


schedule方法

2、


scheduleAtFixedRate方法

看著兩個方法的代碼,除了方法名和參數(shù)校驗外,基本上差別不大,如果去掉忽略掉隊period的處理,完全就是同一個方法。

我們知道這兩個方法都有同一個特性,就是可以對任務(wù)進(jìn)行周期性執(zhí)行,上Demo。

demo

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

以上demo的執(zhí)行結(jié)果

可以看到,這兩個方法的允許結(jié)果沒有差別。

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

追趕性Demo

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

執(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)行說明:

mainLoop關(guā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ù)加入到隊列中。

rescheduleMin方法

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次,才能追趕上CcurrentTime),所以這邊的追趕性就是這個原理。

上面還有個問題,還沒說明,就是為何調(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)度器。

ScheduledThreadPoolExecutor部分代碼

是從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配置整合使用。

缺點還是線程問題過多咯。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容