前言
現(xiàn)代的應(yīng)用程序早已不是以前的那些由簡(jiǎn)單的增刪改查拼湊而成的程序了,高復(fù)雜性早已是標(biāo)配,而任務(wù)的定時(shí)調(diào)度與執(zhí)行也是對(duì)程序的基本要求了。
很多業(yè)務(wù)需求的實(shí)現(xiàn)都離不開定時(shí)任務(wù),例如,每月一號(hào),移動(dòng)將清空你上月未用完流量,重置套餐流量,以及備忘錄提醒、鬧鐘等功能。
java 系統(tǒng)中主要有三種方式來實(shí)現(xiàn)定時(shí)任務(wù):
- timer和timertask
- scheduledexecutorservice
- 三方框架 quartz
下面我們一個(gè)個(gè)來看。
timer和timertask
先看一個(gè)小 demo,接著我們?cè)賮矸治銎渲性恚?/p>
這種方式的定時(shí)任務(wù)主要用到兩個(gè)類,timer 和 timertask。其中,timertask 繼承接口 runnable,抽象的描述一種任務(wù)類型,我們只要重寫實(shí)現(xiàn)它的 run 方法就可以實(shí)現(xiàn)自定義任務(wù)。
而 timer 就是用于定時(shí)任務(wù)調(diào)度的核心類,demo 中我們調(diào)用其 schedule 并指定延時(shí) 1000 毫秒,所以上述代碼會(huì)在一秒鐘后完成打印操作,接著程序結(jié)束。
那么,使用上很簡(jiǎn)單,兩個(gè)步驟即可,但是其中的實(shí)現(xiàn)邏輯是怎樣的呢?
timer 接口
首先,timer 接口中,這兩個(gè)字段是非常核心重要的:
taskqueue 是一個(gè)隊(duì)列,內(nèi)部由動(dòng)態(tài)數(shù)組實(shí)現(xiàn)的最小堆結(jié)構(gòu),換句話說,它是一個(gè)優(yōu)先級(jí)隊(duì)列。而優(yōu)先級(jí)參考下一次執(zhí)行時(shí)間,越快執(zhí)行的越排在前面,這一點(diǎn)我們回頭再研究。
接著,這個(gè) timerthread 類其實(shí)是 timer 的一個(gè)內(nèi)部類,它繼承了 thread 并重寫了其 run 方法,該線程實(shí)例將在構(gòu)建 timer 實(shí)例的時(shí)候被啟動(dòng)。
run 方法內(nèi)部會(huì)循環(huán)的從隊(duì)列中取任務(wù),如果沒有就阻塞自己,而當(dāng)我們成功的向隊(duì)列中添加了定時(shí)任務(wù),也會(huì)嘗試喚醒該線程。
我們也來看一下 timer 的構(gòu)造方法:
1
2
3
4
|
public timer(string name) { thread.setname(name); thread.start(); } |
再簡(jiǎn)單不過的構(gòu)造函數(shù)了,為內(nèi)部線程設(shè)置線程名,并啟動(dòng)該線程。
最后,我們著重看一下 timer 中用于配置一個(gè)定時(shí)任務(wù)進(jìn)任務(wù)隊(duì)列的方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
//在時(shí)刻 time 處執(zhí)行任務(wù) schedule(timertask task, date time) //延時(shí) delay 毫秒后執(zhí)行任務(wù) schedule(timertask task, long delay) //固定延時(shí)重復(fù)執(zhí)行,firsttime為首次執(zhí)行時(shí)間, //往后沒間隔 period 毫秒執(zhí)行一次 schedule(timertask task, date firsttime, long period) //固定延時(shí)重復(fù)執(zhí)行 //首次執(zhí)行時(shí)間為當(dāng)前時(shí)間延時(shí) delay 毫秒 schedule(timertask task, long delay, long period) //固定頻率重復(fù)執(zhí)行,每過 period 毫秒執(zhí)行一次 scheduleatfixedrate(timertask task, date firsttime, long period) //固定頻率重復(fù)執(zhí)行 scheduleatfixedrate(timertask task, long delay, long period) |
相信有了注釋,這幾個(gè)方法的區(qū)別與作用應(yīng)該不難理解,但是其中有兩個(gè)概念需要作一點(diǎn)區(qū)分。
==固定延時(shí)== vs ==固定頻率==
固定延時(shí):以任務(wù)的上一次 實(shí)際 執(zhí)行時(shí)間做參考,往后延時(shí) period 毫秒。
固定頻率:任務(wù)的往后每一次執(zhí)行時(shí)間都在任務(wù)提交的那一刻得到了確定,不論你上次任務(wù)是否意外延時(shí)了,定時(shí)定點(diǎn)執(zhí)行下一次任務(wù)。
這兩者的區(qū)別還是很大的,希望你能夠理解清楚,接著我們以其中一個(gè)方法為例,看看底層實(shí)現(xiàn)。
以這個(gè)方法為例,其他重載方法的底層調(diào)用都是同樣的,我們不去贅述。
這個(gè)方法的作用,我們?cè)僬f一遍。
以當(dāng)前時(shí)間為準(zhǔn),延時(shí) delay 毫秒后第一次執(zhí)行該任務(wù),并且采取固定延時(shí)的方式,每隔 period 毫秒再次執(zhí)行該任務(wù)。
開頭的兩個(gè)異常判斷我們不再贅述,看看 sched 方法:
方法需要傳入三個(gè)參數(shù),參數(shù) task 代表的需要執(zhí)行的任務(wù)體,timertask 我們回頭會(huì)詳細(xì)介紹,這里你知道它代表了一個(gè)任務(wù)體即可。
參數(shù) time 描述了該任務(wù)下一次執(zhí)行的時(shí)刻,計(jì)算機(jī)底層是以毫秒描述時(shí)刻的,所以這里轉(zhuǎn)換為 long 類型來描述時(shí)刻。
參數(shù) period 是固定延時(shí)的毫秒數(shù)。
整個(gè)方法的邏輯我們可以總結(jié)概括一下,具體的代碼就不一行行分析了,因?yàn)橐膊浑y。
- 首先使用任務(wù)隊(duì)列的內(nèi)置對(duì)象鎖,鎖住個(gè)隊(duì)列。
- 接著再去鎖住我們的 task,并修改其內(nèi)部的一些屬性字段值,nextexecutiontime 指明下一次任務(wù)執(zhí)行時(shí)間,period 設(shè)置固定延時(shí)的毫秒數(shù),修改 state 狀態(tài)為計(jì)劃中。
- 然后將 task 添加到任務(wù)隊(duì)列,其中 add 方法內(nèi)部會(huì)進(jìn)行最小堆重構(gòu),參考的就是 nextexecutiontime 字段的值,越小優(yōu)先級(jí)越高。
- 判斷如果自己就是隊(duì)列第一個(gè)任務(wù),那么將喚醒 timer 中阻塞了的任務(wù)線程。
可能會(huì)有人疑問,timer 如何判斷一個(gè)任務(wù)是否是重復(fù)執(zhí)行的,還是單次執(zhí)行就結(jié)束的?
答案在 timerthread 的 run 方法里,有興趣你可以去研究下,方法體比較多比較長(zhǎng),這里不做分析。
當(dāng)我們構(gòu)造 timer 實(shí)例的時(shí)候,就會(huì)啟動(dòng)該線程,該線程會(huì)在一個(gè)死循環(huán)中嘗試從任務(wù)隊(duì)列上獲取任務(wù),如果成功獲取就執(zhí)行該任務(wù)并在執(zhí)行結(jié)束之后做一個(gè)判斷。
如果 period 值為零,則說明這是一次普通任務(wù),執(zhí)行結(jié)束后將從隊(duì)列首部移除該任務(wù)。
如果 period 為負(fù)值,則說明這是一次固定延時(shí)的任務(wù),修改它下次執(zhí)行時(shí)間 nextexecutiontime 為當(dāng)前時(shí)間減去 period,重構(gòu)任務(wù)隊(duì)列。
如果 period 為正數(shù),則說明這是一次固定頻率的任務(wù),修改它下次執(zhí)行時(shí)間為 上次執(zhí)行時(shí)間加上 period,并重構(gòu)任務(wù)隊(duì)列。
其實(shí),我也已經(jīng)把 timerthread 的 run 方法里最核心的邏輯也已經(jīng)介紹了,建議大家親自去研究研究具體代碼的實(shí)現(xiàn),你會(huì)對(duì)這一塊的邏輯更清晰。
最后,我們看一看這個(gè) timer 它有哪些劣勢(shì)的地方:
- timer 的背后只有一個(gè)線程,不管你有多少個(gè)任務(wù),都只有一個(gè)工作線程,效率上必然是要打折扣的。
- 限于單線程,如果第一個(gè)任務(wù)邏輯上死循環(huán)了,后續(xù)的任務(wù)一個(gè)都得不到執(zhí)行。
- 依然是由于單線程,任一任務(wù)拋出異常后,整個(gè) timer 就會(huì)結(jié)束,后續(xù)任務(wù)全部都無法執(zhí)行。
所以你看,單線程的 timer 帶來了太多局限性,于是我們看它的替代者。
ps:本來計(jì)劃再介紹下 timertask 這個(gè)抽象任務(wù)類的,但是發(fā)現(xiàn)實(shí)在沒啥好介紹的,就是增加了兩個(gè)字段,一個(gè)用于記錄下一次該任務(wù)的執(zhí)行時(shí)間,一個(gè)用于延時(shí)毫秒數(shù)。你也只需要重寫其 run 方法即可。
scheduledexecutorservice
這個(gè)接口相信你一定眼熟,我告訴你在哪見過。
你看,它是我們異步框架中的接口,正好我們今天來介紹他,這樣整個(gè)異步框架中所有的接口我們都分析過了。
scheduledexecutorservice中定義的這四個(gè)接口方法和 timer 中對(duì)應(yīng)的方法幾乎一樣,只不過 timer 的 scheduled 方法需要在外部傳入一個(gè) timertask 的抽象任務(wù)。
而我們的 scheduledexecutorservice 封裝的更加細(xì)致了,隨便你傳 runnable 或是 callable,我會(huì)在內(nèi)部給你做一層封裝,封裝一個(gè)類似 timertask 的抽象任務(wù)類(scheduledfuturetask)。
然后傳入線程池,啟動(dòng)線程去執(zhí)行該任務(wù),而我們的 scheduledfuturetask 重寫的 run 方法是這樣的:
如果 periodic 為 true 則說明這是一個(gè)需要重復(fù)執(zhí)行的任務(wù),否則說明是一個(gè)一次性任務(wù)。
所以實(shí)際執(zhí)行該任務(wù)的時(shí)候,需要分類,如果是普通的任務(wù)就直接調(diào)用 run 方法執(zhí)行即可,否則在執(zhí)行結(jié)束之后還需要重置下下一次執(zhí)行時(shí)間。
整體來說,scheduledexecutorservice 區(qū)別于 timer 的地方就在于前者依賴了線程池來執(zhí)行任務(wù),而任務(wù)本身會(huì)判斷是什么類型的任務(wù),需要重復(fù)執(zhí)行的在任務(wù)執(zhí)行結(jié)束后會(huì)被重新添加到任務(wù)隊(duì)列。
而對(duì)于后者來說,它只依賴一個(gè)線程不停的去獲取隊(duì)列首部的任務(wù)并嘗試執(zhí)行它,無論是效率上、還是安全性上都比不上前者。
所以,建議使用 scheduledexecutorservice 取代 timer,當(dāng)然,通過學(xué)習(xí) timer 會(huì)更有助于對(duì) scheduledexecutorservice 的研究。
三方框架 quartz
除了上述兩種定時(shí)任務(wù)框架外,java 生態(tài)圈還存在一種開源的三方框架,他就是 quartz。
quartz 是一個(gè)功能完善的任務(wù)調(diào)度框架,支持集群環(huán)境下的任務(wù)調(diào)度,需要將任務(wù)調(diào)度狀態(tài)序列化到數(shù)據(jù)庫。
quartz 已經(jīng)是隨著分布式概念的流行,成為企業(yè)級(jí)定時(shí)任務(wù)調(diào)度框架中的不二選擇。
quartz 這個(gè)框架的使用及與原理在本篇就不做介紹了,我們會(huì)在后續(xù)介紹分布式概念的時(shí)候再來介紹它與 springcloud 平臺(tái)下的整合使用情況。
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問大家可以留言交流,謝謝大家對(duì)服務(wù)器之家的支持。
原文鏈接:https://www.cnblogs.com/yangming1996/p/10317949.html