Java中Timer 類的用法

來源:互聯網
上載者:User

  Java中Timer 類的用法

我將把 java.util.Timer 和 java.util.TimerTask 統稱為 Java 計時器架構,它們使程式員可以很容易地計劃簡單的任務(注意這些類也可用於 J2ME 中)。在 Java 2 SDK, Standard Edition, Version 1.3 中引入這個架構之前,開發人員必須編寫自己的發送器,這需要花費很大精力來處理線程和複雜的 Object.wait() 方法。不過,Java 計時器架構沒有足夠的能力來滿足許多應用程式的計劃要求。甚至一項需要在每天同一時間重複執行的任務,也不能直接使用 Timer 來計劃,因為在夏令時開始和結束時會出現時間跳躍。

本文展示了一個通用的 TimerTimerTask 計劃架構,從而允許更靈活的計劃任務。這個架構非常簡單 ―― 它包括兩個類和一個介面 ―― 並且容易掌握。如果您習慣於使用 Java 定時器架構,那麼您應該可以很快地掌握這個計劃架構。

計劃單次任務

計劃架構建立在 Java 定時器架構類的基礎之上。因此,在解釋如何使用計劃架構以及如何?它之前,我們將首先看看如何用這些類進行計劃。

想像一個煮蛋計時器,在數分鐘之後(這時蛋煮好了)它會發出聲音提醒您。清單 1 中的代碼構成了一個簡單的煮蛋計時器的基本結構,它用 Java 語言編寫:

清單 1. EggTimer 類

package org.tiling.scheduling.examples;import java.util.Timer;import java.util.TimerTask;public class EggTimer {    private final Timer timer = new Timer();    private final int minutes;    public EggTimer(int minutes) {        this.minutes = minutes;    }    public void start() {        timer.schedule(new TimerTask() {            public void run() {                playSound();                timer.cancel();            }            private void playSound() {                System.out.println("Your egg is ready!");                // Start a new thread to play a sound...            }        }, minutes * 60 * 1000);    }    public static void main(String[] args) {        EggTimer eggTimer = new EggTimer(2);        eggTimer.start();    }}

EggTimer 執行個體擁有一個 Timer 執行個體,用於提供必要的計劃。用 start() 方法啟動煮蛋計時器後,它就計划了一個 TimerTask ,在指定的分鐘數之後執行。時間到了, Timer 就在後台調用 TimerTaskstart() 方法,這會使它發出聲音。在取消計時器後這個應用程式就會中止。

計劃重複執行的任務

通過指定一個固定的執行頻率或者固定的執行時間間隔, Timer 可以對重複執行的任務進行計劃。不過,有許多應用程式要求更複雜的計劃。例如,每天清晨在同一時間發出叫醒鈴聲的鬧鐘不能簡單地使用固定的計劃頻率 86400000 毫秒(24 小時),因為在鐘撥快或者撥慢(如果您的時區使用夏令時)的那些天裡,叫醒可能過晚或者過早。解決方案是使用日曆演算法計算每日事件下一次計劃發生的時間。而這正是計劃架構所支援的。考慮清單 2 中的 AlarmClock 實現(有關計劃架構的原始碼以及包含這個架構和例子的 JAR 檔案,請參閱 參考資料):

清單 2. AlarmClock 類

package org.tiling.scheduling.examples;import java.text.SimpleDateFormat;import java.util.Date;import org.tiling.scheduling.Scheduler;import org.tiling.scheduling.SchedulerTask;import org.tiling.scheduling.examples.iterators.DailyIterator;public class AlarmClock {    private final Scheduler scheduler = new Scheduler();    private final SimpleDateFormat dateFormat =        new SimpleDateFormat("dd MMM yyyy HH:mm:ss.SSS");    private final int hourOfDay, minute, second;    public AlarmClock(int hourOfDay, int minute, int second) {        this.hourOfDay = hourOfDay;        this.minute = minute;        this.second = second;    }    public void start() {        scheduler.schedule(new SchedulerTask() {            public void run() {                soundAlarm();            }            private void soundAlarm() {                System.out.println("Wake up! " +                    "It's " + dateFormat.format(new Date()));                // Start a new thread to sound an alarm...            }        }, new DailyIterator(hourOfDay, minute, second));    }    public static void main(String[] args) {        AlarmClock alarmClock = new AlarmClock(7, 0, 0);        alarmClock.start();    }}

注意這段代碼與煮蛋計時器應用程式非常相似。 AlarmClock 執行個體擁有一個 Scheduler (而不是 Timer )執行個體,用於提供必要的計劃。啟動後,這個鬧鐘對 SchedulerTask (而不是 TimerTask )進行調度用以發出警示聲。這個鬧鐘不是計劃一個任務在固定的延遲時間後執行,而是用 DailyIterator 類描述其計劃。在這裡,它只是計劃任務在每天上午 7:00 執行。下面是一個正常運行情況下的輸出:

Wake up! It's 24 Aug 2003 07:00:00.023Wake up! It's 25 Aug 2003 07:00:00.001Wake up! It's 26 Aug 2003 07:00:00.058Wake up! It's 27 Aug 2003 07:00:00.015Wake up! It's 28 Aug 2003 07:00:00.002...

DailyIterator 實現了 ScheduleIterator ,這是一個將 SchedulerTask 的計劃執行時間指定為一系列 java.util.Date 對象的介面。然後 next() 方法按時間先後順序迭代 Date 對象。傳回值 null 會使任務取消(即它再也不會運行)―― 這樣的話,試圖再次計劃將會拋出一個異常。清單 3 包含 ScheduleIterator 介面:

清單 3. ScheduleIterator 介面

package org.tiling.scheduling;import java.util.Date;public interface ScheduleIterator {    public Date next();}

DailyIterator next() 方法返回表示每天同一時間(上午 7:00)的 Date 對象,如清單 4 所示。所以,如果對新構建的 next() 類調用 next() ,那麼將會得到傳遞給建構函式的那個日期當天或者後面一天的 7:00 AM。再次調用 next() 會返回後一天的 7:00 AM,如此重複。為了實現這種行為, DailyIterator 使用了 java.util.Calendar 執行個體。建構函式會在日曆中加上一天,對日曆的這種設定使得第一次調用 next() 會返回正確的 Date 。注意代碼沒有明確地提到夏令時修正,因為 Calendar 實現(在本例中是 GregorianCalendar )負責對此進行處理,所以不需要這樣做。

清單 4. DailyIterator 類

package org.tiling.scheduling.examples.iterators;import org.tiling.scheduling.ScheduleIterator;import java.util.Calendar;import java.util.Date;/** * A DailyIterator class returns a sequence of dates on subsequent days * representing the same time each day. */public class DailyIterator implements ScheduleIterator {    private final int hourOfDay, minute, second;    private final Calendar calendar = Calendar.getInstance();    public DailyIterator(int hourOfDay, int minute, int second) {        this(hourOfDay, minute, second, new Date());    }    public DailyIterator(int hourOfDay, int minute, int second, Date date) {        this.hourOfDay = hourOfDay;        this.minute = minute;        this.second = second;        calendar.setTime(date);        calendar.set(Calendar.HOUR_OF_DAY, hourOfDay);        calendar.set(Calendar.MINUTE, minute);        calendar.set(Calendar.SECOND, second);        calendar.set(Calendar.MILLISECOND, 0);        if (!calendar.getTime().before(date)) {            calendar.add(Calendar.DATE, -1);        }    }    public Date next() {        calendar.add(Calendar.DATE, 1);        return calendar.getTime();    }}

實現計劃架構

在上一節,我們學習了如何使用計劃架構,並將它與 Java 定時器架構進行了比較。下面,我將向您展示如何?這個架構。除了 清單 3 中展示的 ScheduleIterator 介面,構成這個架構的還有另外兩個類 ―― SchedulerSchedulerTask 。這些類實際上在內部使用 TimerSchedulerTask ,因為計劃其實就是一系列的單次定時器。清單 5 和 6 顯示了這兩個類的原始碼:

清單 5. Scheduler

package org.tiling.scheduling;import java.util.Date;import java.util.Timer;import java.util.TimerTask;public class Scheduler {    class SchedulerTimerTask extends TimerTask {        private SchedulerTask schedulerTask;        private ScheduleIterator iterator;        public SchedulerTimerTask(SchedulerTask schedulerTask,                ScheduleIterator iterator) {            this.schedulerTask = schedulerTask;            this.iterator = iterator;        }        public void run() {            schedulerTask.run();            reschedule(schedulerTask, iterator);        }    }    private final Timer timer = new Timer();    public Scheduler() {    }    public void cancel() {        timer.cancel();    }    public void schedule(SchedulerTask schedulerTask,            ScheduleIterator iterator) {        Date time = iterator.next();        if (time == null) {            schedulerTask.cancel();        } else {            synchronized(schedulerTask.lock) {                if (schedulerTask.state != SchedulerTask.VIRGIN) {                  throw new IllegalStateException("Task already                   scheduled " + "or cancelled");                }                schedulerTask.state = SchedulerTask.SCHEDULED;                schedulerTask.timerTask =                    new SchedulerTimerTask(schedulerTask, iterator);                timer.schedule(schedulerTask.timerTask, time);            }        }    }    private void reschedule(SchedulerTask schedulerTask,            ScheduleIterator iterator) {        Date time = iterator.next();        if (time == null) {            schedulerTask.cancel();        } else {            synchronized(schedulerTask.lock) {                if (schedulerTask.state != SchedulerTask.CANCELLED) {                    schedulerTask.timerTask =                        new SchedulerTimerTask(schedulerTask, iterator);                    timer.schedule(schedulerTask.timerTask, time);                }            }        }    }}

清單 6 顯示了 SchedulerTask 類的原始碼:

清單 6. SchedulerTask

package org.tiling.scheduling;import java.util.TimerTask;public abstract class SchedulerTask implements Runnable {    final Object lock = new Object();    int state = VIRGIN;    static final int VIRGIN = 0;    static final int SCHEDULED = 1;    static final int CANCELLED = 2;    TimerTask timerTask;    protected SchedulerTask() {    }    public abstract void run();    public boolean cancel() {        synchronized(lock) {            if (timerTask != null) {                timerTask.cancel();            }            boolean result = (state == SCHEDULED);            state = CANCELLED;            return result;        }    }    public long scheduledExecutionTime() {        synchronized(lock) {         return timerTask == null ? 0 : timerTask.scheduledExecutionTime();        }    }}

就像煮蛋計時器, Scheduler 的每一個執行個體都擁有 Timer 的一個執行個體,用於提供底層計劃。 Scheduler 並沒有像實現煮蛋計時器時那樣使用一個單次定時器,它將一組單次定時器串接在一起,以便在由 ScheduleIterator 指定的各個時間執行 SchedulerTask 類。

考慮 Scheduler 上的 public schedule() 方法 ―― 這是計劃的進入點,因為它是客戶調用的方法(在 取消任務 一節中將描述僅有的另一個 public 方法 cancel() )。通過調用 ScheduleIterator 介面的 next() ,發現第一次執行 SchedulerTask 的時間。然後通過調用底層 Timer 類的單次 schedule() 方法,啟動計劃在這一時刻執行。為單次執行提供的 TimerTask 對象是嵌入的 SchedulerTimerTask 類的一個執行個體,它封裝了任務和迭代器(iterator)。在指定的時間,調用嵌入類的 run() 方法,它使用封裝的任務和迭代器引用以便重新計劃任務的下一次執行。 reschedule() 方法與 schedule() 方法非常相似,只不過它是 private 的,並且執行一組稍有不同的 SchedulerTask 狀態檢查。重新計划過程反覆重複,為每次計劃執行構造一個新的嵌入類執行個體,直到任務或者發送器被取消(或者 JVM 關閉)。

類似於 TimerTaskSchedulerTask 在其生命週期中要經曆一系列的狀態。建立後,它處於 VIRGIN 狀態,這表明它從沒有計划過。計劃以後,它就變為 SCHEDULED 狀態,再用下面描述的方法之一取消任務後,它就變為 CANCELLED 狀態。管理正確的狀態轉變 ―― 如保證不對一個非 VIRGIN 狀態的任務進行兩次計劃 ―― 增加了 SchedulerSchedulerTask 類的複雜性。在進行可能改變任務狀態的操作時,代碼必須同步任務的鎖對象。

取消任務

取消計劃任務有三種方式。第一種是調用 SchedulerTaskcancel() 方法。這很像調用 TimerTaskcancel() 方法:任務再也不會運行了,不過已經啟動並執行任務 仍會運行完成。 cancel() 方法的傳回值是一個布爾值,表示如果沒有調用 cancel() 的話,計劃的任務是否還會運行。更準確地說,如果任務在調用 cancel() 之前是 SCHEDULED 狀態,那麼它就返回 true 。如果試圖再次計劃一個取消的(甚至是已計劃的)任務,那麼 Scheduler 就會拋出一個 IllegalStateException

取消計劃任務的第二種方式是讓 ScheduleIterator 返回 null 。這隻是第一種方式的簡化操作,因為 Scheduler 類調用 SchedulerTask 類的 cancel() 方法。如果您想用迭代器而不是任務來控制計劃停止時間時,就用得上這種取消任務的方式了。

第三種方式是通過調用其 cancel() 方法取消整個 Scheduler 。這會取消偵錯工具的所有任務,並使它不能再計劃任何任務。

擴充 cron 公用程式

可以將計劃架構比作 UNIX 的 cron 公用程式,只不過計劃次數的規定是強制性而不是聲明性的。例如,在 AlarmClock 實現中使用的 DailyIterator 類,它的計劃與 cron 作業的計劃相同,都是由以 0 7 * * * 開始的 crontab 項指定的(這些欄位分別指定分鐘、小時、日、月和星期)。

不過,計劃架構比 cron 更靈活。想像一個在早晨開啟熱水的 HeatingController 應用程式。我想指示它“在每個工作日上午 8:00 開啟熱水,在周未上午 9:00 開啟熱水”。使用 cron ,我需要兩個 crontab 項( 0 8 * * 1,2,3,4,50 9 * * 6,7 )。而使用 ScheduleIterator 的解決方案更簡潔一些,因為我可以使用複合(composition)來定義單一迭代器。清單 7 顯示了其中的一種方法:

清單 7. 用複合定義單一迭代器

    int[] weekdays = new int[] {        Calendar.MONDAY,        Calendar.TUESDAY,        Calendar.WEDNESDAY,        Calendar.THURSDAY,        Calendar.FRIDAY    };    int[] weekend = new int[] {        Calendar.SATURDAY,        Calendar.SUNDAY    };    ScheduleIterator i = new CompositeIterator(        new ScheduleIterator[] {            new RestrictedDailyIterator(8, 0, 0, weekdays),            new RestrictedDailyIterator(9, 0, 0, weekend)        }    );

RestrictedDailyIterator 類很像 DailyIterator ,只不過它限制為只在一周的特定日子裡運行,而一個 CompositeIterator 類取得一組 ScheduleIterator s,並將日期正確排列到單個計劃中。。

有許多計劃是 cron 無法產生的,但是 ScheduleIterator 實現卻可以。例如,“每個月的最後一天”描述的計劃可以用標準 Java 日曆演算法來實現(用 Calendar 類),而用 cron 則無法表達它。應用程式甚至無需使用 Calendar 類。在本文的原始碼中,我加入了一個安全燈控制器的例子,它按“在日落之前 15 分鐘開燈”這一計劃運行。這個實現使用了 Calendrical Calculations Software Package ,用於計算當地(給定經度和緯度)的日落時間。

即時保證

在編寫使用計劃的應用程式時,一定要瞭解架構在時間方面有什麼保證。我的工作是提前還是順延強制?如果有提前或者延遲,偏差最大值是多少?不幸的是,對這些問題沒有簡單的答案。不過在實際中,它的行為對於很多應用程式已經足夠了。下面的討論假設系統時鐘是正確的(有關網路時間協議(Network Time Protocol)的資訊,請參閱 參考資料)。

因為 Scheduler 將計劃委託給 Timer 類, Scheduler 可以做出的即時保證與 Timer 的一樣。 TimerObject.wait(long) 方法計劃任務。當前線程要等待直到喚醒它,喚醒可能出於以下原因之一:

  1. 另一個線程調用對象的 notify() 或者 notifyAll() 方法。
  2. 線程被另一個線程中斷。
  3. 在沒有通知的情況下,線程被喚醒(稱為 spurious wakeup,Joshua Bloch 的 Effective Java Programming Language Guide一書中 Item 50 對其進行了描述 ―― 請參閱 參考資料)。
  4. 規定的時間已到。

對於 Timer 類來說,第一種可能性是不會發生的,因為對其調用 wait() 的對象是私人的。即便如此, Timer 實現仍然針對前三種提前喚醒的原因進行了保護,這樣保證了線程在規定時間後才喚醒。目前, Object.wait(long) 的文檔注釋聲明,它會在規定的時間“前後”蘇醒,所以線程有可能提前喚醒。在本例中, Timer 會讓另一個 wait() 執行( scheduledExecutionTime - System.currentTimeMillis() )毫秒,從而保證 任務永遠不會提前執行

任務是否會順延強制呢?會的。順延強制有兩個主要原因:線程計劃和垃圾收集。

Java 語言規範故意沒有對線程計劃做嚴格的規定。這是因為 Java 平台是通用的,並針對於大範圍的硬體及其相關的作業系統。雖然大多數 JVM 實現都有公平的線程發送器,但是這一點沒有任何保證 —— 當然,各個實現都有不同的為線程分配處理器時間的策略。因此,當 Timer 線程在分配的時間後喚醒時,它實際執行其任務的時間取決於 JVM 的線程計劃策略,以及有多少其他線程競爭處理器時間。因此,要減緩任務的順延強制,應該將應用程式中可啟動並執行線程數降至最少。為了做到這一點,可以考慮在一個單獨的 JVM 中運行發送器。

對於建立大量對象的大型應用程式,JVM 花在垃圾收集(GC)上的時間會非常多。預設情況下,進行 GC 時,整個應用程式都必須等待它完成,這可能要有幾秒鐘甚至更長的時間( Java 應用程式啟動器的命令列選項 -verbose:gc 將導致向控制台報告每一次 GC 事件)。要將這些由 GC 引起的暫停(這可能會影響快速任務的執行)降至最少,應該將應用程式建立的對象的數目降至最低。同樣,在單獨的 JVM 中運行計劃代碼是有協助的。同時,可以試用幾個微調選項以儘可能地減少 GC 暫停。例如,增量 GC 會盡量將主收集的代價分散到幾個小的收集上。當然這會降低 GC 的效率,但是這可能是時間計劃的一個可接受的代價。

我被計划到什麼時候?

如果任務本身能監視並記錄所有順延強制的執行個體,那麼對於確定任務是否能按時運行會很有協助。 SchedulerTask 類似於 TimerTask ,有一個 scheduledExecutionTime() 方法,它返回計劃任務最近一次執行的時間。在任務的 run() 方法開始時,對錶達式 System.currentTimeMillis() - scheduledExecutionTime() 進行判斷,可以讓您確定任務延遲了多久執行(以毫秒為單位)。可以記錄這個值,以便產生一個關於順延強制的分布統計。可以用這個值決定任務應當採取什麼動作 ―― 例如,如果任務太遲了,那麼它可能什麼也不做。在遵循上述原則的情況下,如果應用程式需要更嚴格的時間保證,可參考 Java 的即時規範。

結束語

在本文中,我介紹了 Java 定時器架構的一個簡單增強,它使得靈活的計劃策略成為可能。新的架構實質上是更通用的 cron ―― 事實上,將 cron 實現為一個 ScheduleIterator 介面,用以替換單純的 Java cron ,這是非常有用的。雖然沒有提供嚴格的即時保證,但是許多需要計劃定期任務的通用 Java 應用程式都可以使用這一架構。

 

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.