實(shí)現(xiàn)計(jì)劃框架
在上一節(jié),我們學(xué)習(xí)了如何使用計(jì)劃框架,并將它與 Java 定時(shí)器框架進(jìn)行了比較。下面,我將向您展示如何實(shí)現(xiàn)這個(gè)框架。除了 清單 3 中展示的 ScheduleIterator 接口,構(gòu)成這個(gè)框架的還有另外兩個(gè)類(lèi) —— Scheduler 和 SchedulerTask 。這些類(lèi)實(shí)際上在內(nèi)部使用 Timer 和 SchedulerTask,因?yàn)橛?jì)劃其實(shí)就是一系列的單次定時(shí)器。清單 5 和 6 顯示了這兩個(gè)類(lèi)的源代碼:
清單 5. Scheduler
清單 6 顯示了 SchedulerTask 類(lèi)的源代碼:
就像煮蛋計(jì)時(shí)器,Scheduler 的每一個(gè)實(shí)例都擁有 Timer 的一個(gè)實(shí)例,用于提供底層計(jì)劃。Scheduler 并沒(méi)有像實(shí)現(xiàn)煮蛋計(jì)時(shí)器時(shí)那樣使用一個(gè)單次定時(shí)器,它將一組單次定時(shí)器串接在一起,以便在由 ScheduleIterator 指定的各個(gè)時(shí)間執(zhí)行 SchedulerTask 類(lèi)。
考慮 Scheduler 上的 public schedule() 方法 —— 這是計(jì)劃的入口點(diǎn),因?yàn)樗强蛻?hù)調(diào)用的方法(在 取消任務(wù) 一節(jié)中將描述僅有的另一個(gè) public 方法 cancel())。通過(guò)調(diào)用 ScheduleIterator 接口的 next(),發(fā)現(xiàn)第一次執(zhí)行 SchedulerTask 的時(shí)間。然后通過(guò)調(diào)用底層 Timer 類(lèi)的單次 schedule() 方法,啟動(dòng)計(jì)劃在這一時(shí)刻執(zhí)行。為單次執(zhí)行提供的 TimerTask 對(duì)象是嵌入的 SchedulerTimerTask 類(lèi)的一個(gè)實(shí)例,它包裝了任務(wù)和迭代器(iterator)。在指定的時(shí)間,調(diào)用嵌入類(lèi)的 run() 方法,它使用包裝的任務(wù)和迭代器引用以便重新計(jì)劃任務(wù)的下一次執(zhí)行。reschedule() 方法與 schedule() 方法非常相似,只不過(guò)它是 private 的,并且執(zhí)行一組稍有不同的 SchedulerTask 狀態(tài)檢查。重新計(jì)劃過(guò)程反復(fù)重復(fù),為每次計(jì)劃執(zhí)行構(gòu)造一個(gè)新的嵌入類(lèi)實(shí)例,直到任務(wù)或者調(diào)度程序被取消(或者 JVM 關(guān)閉)。
類(lèi)似于 TimerTask,SchedulerTask 在其生命周期中要經(jīng)歷一系列的狀態(tài)。創(chuàng)建后,它處于 VIRGIN 狀態(tài),這表明它從沒(méi)有計(jì)劃過(guò)。計(jì)劃以后,它就變?yōu)?SCHEDULED 狀態(tài),再用下面描述的方法之一取消任務(wù)后,它就變?yōu)?CANCELLED 狀態(tài)。管理正確的狀態(tài)轉(zhuǎn)變 —— 如保證不對(duì)一個(gè)非 VIRGIN 狀態(tài)的任務(wù)進(jìn)行兩次計(jì)劃 —— 增加了 Scheduler 和 SchedulerTask 類(lèi)的復(fù)雜性。在進(jìn)行可能改變?nèi)蝿?wù)狀態(tài)的操作時(shí),代碼必須同步任務(wù)的鎖對(duì)象。
取消任務(wù)
取消計(jì)劃任務(wù)有三種方式。第一種是調(diào)用 SchedulerTask 的 cancel() 方法。這很像調(diào)用 TimerTask 的 cancel()方法:任務(wù)再也不會(huì)運(yùn)行了,不過(guò)已經(jīng)運(yùn)行的任務(wù)仍會(huì)運(yùn)行完成。 cancel() 方法的返回值是一個(gè)布爾值,表示如果沒(méi)有調(diào)用 cancel() 的話(huà),計(jì)劃的任務(wù)是否還會(huì)運(yùn)行。更準(zhǔn)確地說(shuō),如果任務(wù)在調(diào)用 cancel() 之前是 SCHEDULED 狀態(tài),那么它就返回 true。如果試圖再次計(jì)劃一個(gè)取消的(甚至是已計(jì)劃的)任務(wù),那么 Scheduler 就會(huì)拋出一個(gè) IllegalStateException。
取消計(jì)劃任務(wù)的第二種方式是讓 ScheduleIterator 返回 null。這只是第一種方式的簡(jiǎn)化操作,因?yàn)?Scheduler 類(lèi)調(diào)用 SchedulerTask 類(lèi)的 cancel()方法。如果您想用迭代器而不是任務(wù)來(lái)控制計(jì)劃停止時(shí)間時(shí),就用得上這種取消任務(wù)的方式了。
第三種方式是通過(guò)調(diào)用其 cancel() 方法取消整個(gè) Scheduler。這會(huì)取消調(diào)試程序的所有任務(wù),并使它不能再計(jì)劃任何任務(wù)。
擴(kuò)展 cron 實(shí)用程序
可以將計(jì)劃框架比作 UNIX 的 cron 實(shí)用程序,只不過(guò)計(jì)劃次數(shù)的規(guī)定是強(qiáng)制性而不是聲明性的。例如,在 AlarmClock 實(shí)現(xiàn)中使用的 DailyIterator 類(lèi),它的計(jì)劃與 cron 作業(yè)的計(jì)劃相同,都是由以 0 7 * * * 開(kāi)始的 crontab 項(xiàng)指定的(這些字段分別指定分鐘、小時(shí)、日、月和星期)。
不過(guò),計(jì)劃框架比 cron 更靈活。想像一個(gè)在早晨打開(kāi)熱水的 HeatingController 應(yīng)用程序。我想指示它“在每個(gè)工作日上午 8:00 打開(kāi)熱水,在周未上午 9:00 打開(kāi)熱水”。使用 cron,我需要兩個(gè) crontab 項(xiàng)(0 8 * * 1,2,3,4,5 和 0 9 * * 6,7)。而使用 ScheduleIterator 的解決方案更簡(jiǎn)潔一些,因?yàn)槲铱梢允褂脧?fù)合(composition)來(lái)定義單一迭代器。清單 7 顯示了其中的一種方法:
清單 7. 用復(fù)合定義單一迭代器
RestrictedDailyIterator 類(lèi)很像 DailyIterator,只不過(guò)它限制為只在一周的特定日子里運(yùn)行,而一個(gè) CompositeIterator 類(lèi)取得一組 ScheduleIterators,并將日期正確排列到單個(gè)計(jì)劃中。
有許多計(jì)劃是 cron 無(wú)法生成的,但是 ScheduleIterator 實(shí)現(xiàn)卻可以。例如,“每個(gè)月的最后一天”描述的計(jì)劃可以用標(biāo)準(zhǔn) Java 日歷算法來(lái)實(shí)現(xiàn)(用 Calendar 類(lèi)),而用 cron 則無(wú)法表達(dá)它。應(yīng)用程序甚至無(wú)需使用 Calendar 類(lèi)。在本文的源代碼(請(qǐng)參閱 參考資料)中,我加入了一個(gè)安全燈控制器的例子,它按“在日落之前 15 分鐘開(kāi)燈”這一計(jì)劃運(yùn)行。這個(gè)實(shí)現(xiàn)使用了 Calendrical Calculations Software Package,用于計(jì)算當(dāng)?shù)兀ńo定經(jīng)度和緯度)的日落時(shí)間。
實(shí)時(shí)保證
在編寫(xiě)使用計(jì)劃的應(yīng)用程序時(shí),一定要了解框架在時(shí)間方面有什么保證。我的任務(wù)是提前還是延遲執(zhí)行?如果有提前或者延遲,偏差最大值是多少?不幸的是,對(duì)這些問(wèn)題沒(méi)有簡(jiǎn)單的答案。不過(guò)在實(shí)際中,它的行為對(duì)于很多應(yīng)用程序已經(jīng)足夠了。下面的討論假設(shè)系統(tǒng)時(shí)鐘是正確的。
因?yàn)?Scheduler 將計(jì)劃委托給 Timer 類(lèi),Scheduler 可以做出的實(shí)時(shí)保證與 Timer 的一樣。Timer 用 Object.wait(long) 方法計(jì)劃任務(wù)。當(dāng)前線(xiàn)程要等待直到喚醒它,喚醒可能出于以下原因之一:
1.另一個(gè)線(xiàn)程調(diào)用對(duì)象的 notify() 或者 notifyAll() 方法。
2.線(xiàn)程被另一個(gè)線(xiàn)程中斷。
3.在沒(méi)有通知的情況下,線(xiàn)程被喚醒(稱(chēng)為 spurious wakeup,Joshua Bloch 的 Effective Java Programming Language Guide 一書(shū)中 Item 50 對(duì)其進(jìn)行了描述 。
4.規(guī)定的時(shí)間已到。
對(duì)于 Timer 類(lèi)來(lái)說(shuō),第一種可能性是不會(huì)發(fā)生的,因?yàn)閷?duì)其調(diào)用 wait() 的對(duì)象是私有的。即便如此,Timer 實(shí)現(xiàn)仍然針對(duì)前三種提前喚醒的原因進(jìn)行了保護(hù),這樣保證了線(xiàn)程在規(guī)定時(shí)間后才喚醒。目前,Object.wait(long) 的文檔注釋聲明,它會(huì)在規(guī)定的時(shí)間“前后”蘇醒,所以線(xiàn)程有可能提前喚醒。在本例中,Timer 會(huì)讓另一個(gè) wait() 執(zhí)行(scheduledExecutionTime - System.currentTimeMillis())毫秒,從而保證任務(wù)永遠(yuǎn)不會(huì)提前執(zhí)行。任務(wù)是否會(huì)延遲執(zhí)行呢?會(huì)的。延遲執(zhí)行有兩個(gè)主要原因:線(xiàn) 程計(jì)劃和垃圾收集。
Java 語(yǔ)言規(guī)范故意沒(méi)有對(duì)線(xiàn)程計(jì)劃做嚴(yán)格的規(guī)定。這是因?yàn)?Java 平臺(tái)是通用的,并針對(duì)于大范圍的硬件及其相關(guān)的操作系統(tǒng)。雖然大多數(shù) JVM 實(shí)現(xiàn)都有公平的線(xiàn)程調(diào)度程序,但是這一點(diǎn)沒(méi)有任何保證 —— 當(dāng)然,各個(gè)實(shí)現(xiàn)都有不同的為線(xiàn)程分配處理器時(shí)間的策略。因此,當(dāng) Timer 線(xiàn)程在分配的時(shí)間后喚醒時(shí),它實(shí)際執(zhí)行其任務(wù)的時(shí)間取決于 JVM 的線(xiàn)程計(jì)劃策略,以及有多少其他線(xiàn)程競(jìng)爭(zhēng)處理器時(shí)間。因此,要減緩任務(wù)的延遲執(zhí)行,應(yīng)該將應(yīng)用程序中可運(yùn)行的線(xiàn)程數(shù)降至最少。為了做到這一點(diǎn),可以考慮在 一個(gè)單獨(dú)的 JVM 中運(yùn)行調(diào)度程序。
對(duì)于創(chuàng)建大量對(duì)象的大型應(yīng)用程序,JVM 花在垃圾收集(GC)上的時(shí)間會(huì)非常多。默認(rèn)情況下,進(jìn)行 GC 時(shí),整個(gè)應(yīng)用程序都必須等待它完成,這可能要有幾秒鐘甚至更長(zhǎng)的時(shí)間(Java 應(yīng)用程序啟動(dòng)器的命令行選項(xiàng) -verbose:gc 將導(dǎo)致向控制臺(tái)報(bào)告每一次 GC 事件)。要將這些由 GC 引起的暫停(這可能會(huì)影響快速任務(wù)的執(zhí)行)降至最少,應(yīng)該將應(yīng)用程序創(chuàng)建的對(duì)象的數(shù)目降至最低。同樣,在單獨(dú)的 JVM 中運(yùn)行計(jì)劃代碼是有幫助的。同時(shí),可以試用幾個(gè)微調(diào)選項(xiàng)以盡可能地減少 GC 暫停。例如,增量 GC 會(huì)盡量將主收集的代價(jià)分散到幾個(gè)小的收集上。當(dāng)然這會(huì)降低 GC 的效率,但是這可能是時(shí)間計(jì)劃的一個(gè)可接受的代價(jià)。
被計(jì)劃到什么時(shí)候?
如果任務(wù)本身能監(jiān)視并記錄所有延遲執(zhí)行的實(shí)例,那么對(duì)于確定任務(wù)是否能按時(shí)運(yùn)行會(huì)很有幫助。SchedulerTask 類(lèi)似于 TimerTask,有一個(gè) scheduledExecutionTime() 方法,它返回計(jì)劃任務(wù)最近一次執(zhí)行的時(shí)間。在任務(wù)的 run() 方法開(kāi)始時(shí),對(duì)表達(dá)式 System.currentTimeMillis() - scheduledExecutionTime() 進(jìn)行判斷,可以讓您確定任務(wù)延遲了多久執(zhí)行(以毫秒為單位)。可以記錄這個(gè)值,以便生成一個(gè)關(guān)于延遲執(zhí)行的分布統(tǒng)計(jì)。可以用這個(gè)值決定任務(wù)應(yīng)當(dāng)采取什么動(dòng) 作 —— 例如,如果任務(wù)太遲了,那么它可能什么也不做。在遵循上述原則的情況下,如果應(yīng)用程序需要更嚴(yán)格的時(shí)間保證,可參考 Java 的實(shí)時(shí)規(guī)范。
結(jié)束語(yǔ)
在本文中,我介紹了 Java 定時(shí)器框架的一個(gè)簡(jiǎn)單增強(qiáng),它使得靈活的計(jì)劃策略成為可能。新的框架實(shí)質(zhì)上是更通用的 cron —— 事實(shí)上,將 cron 實(shí)現(xiàn)為一個(gè) ScheduleIterator 接口,用以替換單純的 Java cron,這是非常有用的。雖然沒(méi)有提供嚴(yán)格的實(shí)時(shí)保證,但是許多需要計(jì)劃定期任務(wù)的通用 Java 應(yīng)用程序都可以使用這一框架。
參考資料
·下載本文中使用的 源代碼。
·“Tuning Garbage Collection with the 1.3.1 Java Virtual Machine”是 Sun 的一篇非常有用的文章,它給出了關(guān)于如何最小化 GC 暫停時(shí)間的提示。
·要獲得 developerWorks 中有關(guān) GC 的更多信息,請(qǐng)參閱以下文章:
“Java 理論與實(shí)踐:垃圾收集簡(jiǎn)史” (2003 年 10 月)。
“Mash that trash”(2003 年 7 月)。
“Fine-tuning Java garbage collection performance”(2003 年 1 月)。
“Sensible sanitation, Part 1”(2002 年 8 月)。
“Sensible sanitation, Part 2”(2002 年 8 月)。
“Sensible sanitation, Part 3”(2002 年 9 月)。
·在“Java 理論與實(shí)踐:并發(fā)在一定程度上使一切變得簡(jiǎn)單”(developerWorks, 2002 年 11 月)中,Brian Goetz 討論了 Doug Lea 的 util.concurrent 庫(kù),這是一個(gè)并發(fā)實(shí)用工具類(lèi)的寶庫(kù)。
·Brian Goetz 的另一篇文章“Threading lightly, Part 2: Reducing contention”(developerWorks,2001 年 9 月)分析了線(xiàn)程競(jìng)用以及如何減少它。
關(guān)于作者
Tom White 是 Kizoom 的首席 Java 開(kāi)發(fā)人員,Kizoom 是一家領(lǐng)先的英國(guó)軟件公司,提供向移動(dòng)設(shè)備發(fā)送個(gè)性化旅行信息的服務(wù)。客戶(hù)包括英國(guó)的國(guó)家火車(chē)操作員、倫敦公共交通系統(tǒng)(national train operator),以及英國(guó)國(guó)家公共汽車(chē)公司。自 1999 年成立以來(lái),Kizoom 使用了極限編程的所有方法。自 1996 年起,Tom 一直全職編寫(xiě) Java 程序,使用了大部分標(biāo)準(zhǔn)和企業(yè) Java API,編寫(xiě)了從客戶(hù) Swing GUI 和圖形到后端消息傳送系統(tǒng)等各種應(yīng)用程序。他在劍橋大學(xué)獲得了一級(jí)榮譽(yù)學(xué)位(first class honours degree)。工作之余,Tom 喜歡逗他的小女兒開(kāi)心,觀(guān)看 20 世紀(jì) 30 年代的好萊塢電影。可以通過(guò) tom@tiling.org 與 Tom 聯(lián)系。
在上一節(jié),我們學(xué)習(xí)了如何使用計(jì)劃框架,并將它與 Java 定時(shí)器框架進(jìn)行了比較。下面,我將向您展示如何實(shí)現(xiàn)這個(gè)框架。除了 清單 3 中展示的 ScheduleIterator 接口,構(gòu)成這個(gè)框架的還有另外兩個(gè)類(lèi) —— Scheduler 和 SchedulerTask 。這些類(lèi)實(shí)際上在內(nèi)部使用 Timer 和 SchedulerTask,因?yàn)橛?jì)劃其實(shí)就是一系列的單次定時(shí)器。清單 5 和 6 顯示了這兩個(gè)類(lèi)的源代碼:
清單 5. Scheduler
|
清單 6 顯示了 SchedulerTask 類(lèi)的源代碼:
|
就像煮蛋計(jì)時(shí)器,Scheduler 的每一個(gè)實(shí)例都擁有 Timer 的一個(gè)實(shí)例,用于提供底層計(jì)劃。Scheduler 并沒(méi)有像實(shí)現(xiàn)煮蛋計(jì)時(shí)器時(shí)那樣使用一個(gè)單次定時(shí)器,它將一組單次定時(shí)器串接在一起,以便在由 ScheduleIterator 指定的各個(gè)時(shí)間執(zhí)行 SchedulerTask 類(lèi)。
考慮 Scheduler 上的 public schedule() 方法 —— 這是計(jì)劃的入口點(diǎn),因?yàn)樗强蛻?hù)調(diào)用的方法(在 取消任務(wù) 一節(jié)中將描述僅有的另一個(gè) public 方法 cancel())。通過(guò)調(diào)用 ScheduleIterator 接口的 next(),發(fā)現(xiàn)第一次執(zhí)行 SchedulerTask 的時(shí)間。然后通過(guò)調(diào)用底層 Timer 類(lèi)的單次 schedule() 方法,啟動(dòng)計(jì)劃在這一時(shí)刻執(zhí)行。為單次執(zhí)行提供的 TimerTask 對(duì)象是嵌入的 SchedulerTimerTask 類(lèi)的一個(gè)實(shí)例,它包裝了任務(wù)和迭代器(iterator)。在指定的時(shí)間,調(diào)用嵌入類(lèi)的 run() 方法,它使用包裝的任務(wù)和迭代器引用以便重新計(jì)劃任務(wù)的下一次執(zhí)行。reschedule() 方法與 schedule() 方法非常相似,只不過(guò)它是 private 的,并且執(zhí)行一組稍有不同的 SchedulerTask 狀態(tài)檢查。重新計(jì)劃過(guò)程反復(fù)重復(fù),為每次計(jì)劃執(zhí)行構(gòu)造一個(gè)新的嵌入類(lèi)實(shí)例,直到任務(wù)或者調(diào)度程序被取消(或者 JVM 關(guān)閉)。
類(lèi)似于 TimerTask,SchedulerTask 在其生命周期中要經(jīng)歷一系列的狀態(tài)。創(chuàng)建后,它處于 VIRGIN 狀態(tài),這表明它從沒(méi)有計(jì)劃過(guò)。計(jì)劃以后,它就變?yōu)?SCHEDULED 狀態(tài),再用下面描述的方法之一取消任務(wù)后,它就變?yōu)?CANCELLED 狀態(tài)。管理正確的狀態(tài)轉(zhuǎn)變 —— 如保證不對(duì)一個(gè)非 VIRGIN 狀態(tài)的任務(wù)進(jìn)行兩次計(jì)劃 —— 增加了 Scheduler 和 SchedulerTask 類(lèi)的復(fù)雜性。在進(jìn)行可能改變?nèi)蝿?wù)狀態(tài)的操作時(shí),代碼必須同步任務(wù)的鎖對(duì)象。
取消任務(wù)
取消計(jì)劃任務(wù)有三種方式。第一種是調(diào)用 SchedulerTask 的 cancel() 方法。這很像調(diào)用 TimerTask 的 cancel()方法:任務(wù)再也不會(huì)運(yùn)行了,不過(guò)已經(jīng)運(yùn)行的任務(wù)仍會(huì)運(yùn)行完成。 cancel() 方法的返回值是一個(gè)布爾值,表示如果沒(méi)有調(diào)用 cancel() 的話(huà),計(jì)劃的任務(wù)是否還會(huì)運(yùn)行。更準(zhǔn)確地說(shuō),如果任務(wù)在調(diào)用 cancel() 之前是 SCHEDULED 狀態(tài),那么它就返回 true。如果試圖再次計(jì)劃一個(gè)取消的(甚至是已計(jì)劃的)任務(wù),那么 Scheduler 就會(huì)拋出一個(gè) IllegalStateException。
取消計(jì)劃任務(wù)的第二種方式是讓 ScheduleIterator 返回 null。這只是第一種方式的簡(jiǎn)化操作,因?yàn)?Scheduler 類(lèi)調(diào)用 SchedulerTask 類(lèi)的 cancel()方法。如果您想用迭代器而不是任務(wù)來(lái)控制計(jì)劃停止時(shí)間時(shí),就用得上這種取消任務(wù)的方式了。
第三種方式是通過(guò)調(diào)用其 cancel() 方法取消整個(gè) Scheduler。這會(huì)取消調(diào)試程序的所有任務(wù),并使它不能再計(jì)劃任何任務(wù)。
擴(kuò)展 cron 實(shí)用程序
可以將計(jì)劃框架比作 UNIX 的 cron 實(shí)用程序,只不過(guò)計(jì)劃次數(shù)的規(guī)定是強(qiáng)制性而不是聲明性的。例如,在 AlarmClock 實(shí)現(xiàn)中使用的 DailyIterator 類(lèi),它的計(jì)劃與 cron 作業(yè)的計(jì)劃相同,都是由以 0 7 * * * 開(kāi)始的 crontab 項(xiàng)指定的(這些字段分別指定分鐘、小時(shí)、日、月和星期)。
不過(guò),計(jì)劃框架比 cron 更靈活。想像一個(gè)在早晨打開(kāi)熱水的 HeatingController 應(yīng)用程序。我想指示它“在每個(gè)工作日上午 8:00 打開(kāi)熱水,在周未上午 9:00 打開(kāi)熱水”。使用 cron,我需要兩個(gè) crontab 項(xiàng)(0 8 * * 1,2,3,4,5 和 0 9 * * 6,7)。而使用 ScheduleIterator 的解決方案更簡(jiǎn)潔一些,因?yàn)槲铱梢允褂脧?fù)合(composition)來(lái)定義單一迭代器。清單 7 顯示了其中的一種方法:
清單 7. 用復(fù)合定義單一迭代器
|
RestrictedDailyIterator 類(lèi)很像 DailyIterator,只不過(guò)它限制為只在一周的特定日子里運(yùn)行,而一個(gè) CompositeIterator 類(lèi)取得一組 ScheduleIterators,并將日期正確排列到單個(gè)計(jì)劃中。
有許多計(jì)劃是 cron 無(wú)法生成的,但是 ScheduleIterator 實(shí)現(xiàn)卻可以。例如,“每個(gè)月的最后一天”描述的計(jì)劃可以用標(biāo)準(zhǔn) Java 日歷算法來(lái)實(shí)現(xiàn)(用 Calendar 類(lèi)),而用 cron 則無(wú)法表達(dá)它。應(yīng)用程序甚至無(wú)需使用 Calendar 類(lèi)。在本文的源代碼(請(qǐng)參閱 參考資料)中,我加入了一個(gè)安全燈控制器的例子,它按“在日落之前 15 分鐘開(kāi)燈”這一計(jì)劃運(yùn)行。這個(gè)實(shí)現(xiàn)使用了 Calendrical Calculations Software Package,用于計(jì)算當(dāng)?shù)兀ńo定經(jīng)度和緯度)的日落時(shí)間。
實(shí)時(shí)保證
在編寫(xiě)使用計(jì)劃的應(yīng)用程序時(shí),一定要了解框架在時(shí)間方面有什么保證。我的任務(wù)是提前還是延遲執(zhí)行?如果有提前或者延遲,偏差最大值是多少?不幸的是,對(duì)這些問(wèn)題沒(méi)有簡(jiǎn)單的答案。不過(guò)在實(shí)際中,它的行為對(duì)于很多應(yīng)用程序已經(jīng)足夠了。下面的討論假設(shè)系統(tǒng)時(shí)鐘是正確的。
因?yàn)?Scheduler 將計(jì)劃委托給 Timer 類(lèi),Scheduler 可以做出的實(shí)時(shí)保證與 Timer 的一樣。Timer 用 Object.wait(long) 方法計(jì)劃任務(wù)。當(dāng)前線(xiàn)程要等待直到喚醒它,喚醒可能出于以下原因之一:
1.另一個(gè)線(xiàn)程調(diào)用對(duì)象的 notify() 或者 notifyAll() 方法。
2.線(xiàn)程被另一個(gè)線(xiàn)程中斷。
3.在沒(méi)有通知的情況下,線(xiàn)程被喚醒(稱(chēng)為 spurious wakeup,Joshua Bloch 的 Effective Java Programming Language Guide 一書(shū)中 Item 50 對(duì)其進(jìn)行了描述 。
4.規(guī)定的時(shí)間已到。
對(duì)于 Timer 類(lèi)來(lái)說(shuō),第一種可能性是不會(huì)發(fā)生的,因?yàn)閷?duì)其調(diào)用 wait() 的對(duì)象是私有的。即便如此,Timer 實(shí)現(xiàn)仍然針對(duì)前三種提前喚醒的原因進(jìn)行了保護(hù),這樣保證了線(xiàn)程在規(guī)定時(shí)間后才喚醒。目前,Object.wait(long) 的文檔注釋聲明,它會(huì)在規(guī)定的時(shí)間“前后”蘇醒,所以線(xiàn)程有可能提前喚醒。在本例中,Timer 會(huì)讓另一個(gè) wait() 執(zhí)行(scheduledExecutionTime - System.currentTimeMillis())毫秒,從而保證任務(wù)永遠(yuǎn)不會(huì)提前執(zhí)行。任務(wù)是否會(huì)延遲執(zhí)行呢?會(huì)的。延遲執(zhí)行有兩個(gè)主要原因:線(xiàn) 程計(jì)劃和垃圾收集。
Java 語(yǔ)言規(guī)范故意沒(méi)有對(duì)線(xiàn)程計(jì)劃做嚴(yán)格的規(guī)定。這是因?yàn)?Java 平臺(tái)是通用的,并針對(duì)于大范圍的硬件及其相關(guān)的操作系統(tǒng)。雖然大多數(shù) JVM 實(shí)現(xiàn)都有公平的線(xiàn)程調(diào)度程序,但是這一點(diǎn)沒(méi)有任何保證 —— 當(dāng)然,各個(gè)實(shí)現(xiàn)都有不同的為線(xiàn)程分配處理器時(shí)間的策略。因此,當(dāng) Timer 線(xiàn)程在分配的時(shí)間后喚醒時(shí),它實(shí)際執(zhí)行其任務(wù)的時(shí)間取決于 JVM 的線(xiàn)程計(jì)劃策略,以及有多少其他線(xiàn)程競(jìng)爭(zhēng)處理器時(shí)間。因此,要減緩任務(wù)的延遲執(zhí)行,應(yīng)該將應(yīng)用程序中可運(yùn)行的線(xiàn)程數(shù)降至最少。為了做到這一點(diǎn),可以考慮在 一個(gè)單獨(dú)的 JVM 中運(yùn)行調(diào)度程序。
對(duì)于創(chuàng)建大量對(duì)象的大型應(yīng)用程序,JVM 花在垃圾收集(GC)上的時(shí)間會(huì)非常多。默認(rèn)情況下,進(jìn)行 GC 時(shí),整個(gè)應(yīng)用程序都必須等待它完成,這可能要有幾秒鐘甚至更長(zhǎng)的時(shí)間(Java 應(yīng)用程序啟動(dòng)器的命令行選項(xiàng) -verbose:gc 將導(dǎo)致向控制臺(tái)報(bào)告每一次 GC 事件)。要將這些由 GC 引起的暫停(這可能會(huì)影響快速任務(wù)的執(zhí)行)降至最少,應(yīng)該將應(yīng)用程序創(chuàng)建的對(duì)象的數(shù)目降至最低。同樣,在單獨(dú)的 JVM 中運(yùn)行計(jì)劃代碼是有幫助的。同時(shí),可以試用幾個(gè)微調(diào)選項(xiàng)以盡可能地減少 GC 暫停。例如,增量 GC 會(huì)盡量將主收集的代價(jià)分散到幾個(gè)小的收集上。當(dāng)然這會(huì)降低 GC 的效率,但是這可能是時(shí)間計(jì)劃的一個(gè)可接受的代價(jià)。
被計(jì)劃到什么時(shí)候?
如果任務(wù)本身能監(jiān)視并記錄所有延遲執(zhí)行的實(shí)例,那么對(duì)于確定任務(wù)是否能按時(shí)運(yùn)行會(huì)很有幫助。SchedulerTask 類(lèi)似于 TimerTask,有一個(gè) scheduledExecutionTime() 方法,它返回計(jì)劃任務(wù)最近一次執(zhí)行的時(shí)間。在任務(wù)的 run() 方法開(kāi)始時(shí),對(duì)表達(dá)式 System.currentTimeMillis() - scheduledExecutionTime() 進(jìn)行判斷,可以讓您確定任務(wù)延遲了多久執(zhí)行(以毫秒為單位)。可以記錄這個(gè)值,以便生成一個(gè)關(guān)于延遲執(zhí)行的分布統(tǒng)計(jì)。可以用這個(gè)值決定任務(wù)應(yīng)當(dāng)采取什么動(dòng) 作 —— 例如,如果任務(wù)太遲了,那么它可能什么也不做。在遵循上述原則的情況下,如果應(yīng)用程序需要更嚴(yán)格的時(shí)間保證,可參考 Java 的實(shí)時(shí)規(guī)范。
結(jié)束語(yǔ)
在本文中,我介紹了 Java 定時(shí)器框架的一個(gè)簡(jiǎn)單增強(qiáng),它使得靈活的計(jì)劃策略成為可能。新的框架實(shí)質(zhì)上是更通用的 cron —— 事實(shí)上,將 cron 實(shí)現(xiàn)為一個(gè) ScheduleIterator 接口,用以替換單純的 Java cron,這是非常有用的。雖然沒(méi)有提供嚴(yán)格的實(shí)時(shí)保證,但是許多需要計(jì)劃定期任務(wù)的通用 Java 應(yīng)用程序都可以使用這一框架。
參考資料
·下載本文中使用的 源代碼。
·“Tuning Garbage Collection with the 1.3.1 Java Virtual Machine”是 Sun 的一篇非常有用的文章,它給出了關(guān)于如何最小化 GC 暫停時(shí)間的提示。
·要獲得 developerWorks 中有關(guān) GC 的更多信息,請(qǐng)參閱以下文章:
“Java 理論與實(shí)踐:垃圾收集簡(jiǎn)史” (2003 年 10 月)。
“Mash that trash”(2003 年 7 月)。
“Fine-tuning Java garbage collection performance”(2003 年 1 月)。
“Sensible sanitation, Part 1”(2002 年 8 月)。
“Sensible sanitation, Part 2”(2002 年 8 月)。
“Sensible sanitation, Part 3”(2002 年 9 月)。
·在“Java 理論與實(shí)踐:并發(fā)在一定程度上使一切變得簡(jiǎn)單”(developerWorks, 2002 年 11 月)中,Brian Goetz 討論了 Doug Lea 的 util.concurrent 庫(kù),這是一個(gè)并發(fā)實(shí)用工具類(lèi)的寶庫(kù)。
·Brian Goetz 的另一篇文章“Threading lightly, Part 2: Reducing contention”(developerWorks,2001 年 9 月)分析了線(xiàn)程競(jìng)用以及如何減少它。
關(guān)于作者
Tom White 是 Kizoom 的首席 Java 開(kāi)發(fā)人員,Kizoom 是一家領(lǐng)先的英國(guó)軟件公司,提供向移動(dòng)設(shè)備發(fā)送個(gè)性化旅行信息的服務(wù)。客戶(hù)包括英國(guó)的國(guó)家火車(chē)操作員、倫敦公共交通系統(tǒng)(national train operator),以及英國(guó)國(guó)家公共汽車(chē)公司。自 1999 年成立以來(lái),Kizoom 使用了極限編程的所有方法。自 1996 年起,Tom 一直全職編寫(xiě) Java 程序,使用了大部分標(biāo)準(zhǔn)和企業(yè) Java API,編寫(xiě)了從客戶(hù) Swing GUI 和圖形到后端消息傳送系統(tǒng)等各種應(yīng)用程序。他在劍橋大學(xué)獲得了一級(jí)榮譽(yù)學(xué)位(first class honours degree)。工作之余,Tom 喜歡逗他的小女兒開(kāi)心,觀(guān)看 20 世紀(jì) 30 年代的好萊塢電影。可以通過(guò) tom@tiling.org 與 Tom 聯(lián)系。