Java Timer使用介紹

2022-11-01 18:01:25

java.util包下提供了對定時任務的支援,涉及2個類:

  1. Timer:定時器類
  2. TimerTask:任務抽象類

使用該定時任務我們需要繼承TimerTask抽象類,覆蓋run方法編寫任務執行程式碼,並利用Timer定時器對TimerTask進行排程。

編寫一個任務:

TimerTask task = new TimerTask() {
    @Override
    public void run() {
        System.out.println(DateUtil.formatNow() + " " + Thread.currentThread().getName() + " task run ");
    }
};

接著使用Timer對TimerTask進行排程,Timer提供了多種方法,可分為一次性任務可重複執行任務

一、一次性任務

一次性任務是指Timer執行一次之後,該任務後續不再執行。

一次性任務包括2個方法,如下:

  1. void schedule(TimerTask task, long delay):延遲delay毫秒後執行一次task
  2. void schedule(TimerTask task, Date time):在指定時間time執行一次task,如果time過期,將會立即執行

二、可重複執行任務

可重複執行任務是指,任務允許按照設定的規則重複執行。

可重複執行任務共有4個方法,分為 固定延時 schedule  固定速率 scheduleAtFixedRate

  1. void schedule(TimerTask task, long delay, long period):延遲delay毫秒後執行task,之後每隔period毫秒執行一次task
  2. void schedule(TimerTask task, Date firstTime, long period):在指定時間time執行一次task,之後每隔period毫秒執行一次task
  3. void scheduleAtFixedRate(TimerTask task, long delay, long period):延遲delay毫秒後執行task,之後每隔period毫秒執行一次task
  4. void scheduleAtFixedRate(TimerTask task, Date firstTime, long period):在指定時間time執行一次task,之後每隔period毫秒執行一次task

範例1:schedule方法,延遲delay毫秒後執行task,之後每隔period毫秒執行一次task

System.out.println("啟動於:" + DateUtil.formatNow());
Timer timer = new Timer("timer");
timer.schedule(task, 1000, 2000);

輸出:

啟動於:2022-10-31 10:05:15
2022-10-31 10:05:16 timer task run 
2022-10-31 10:05:18 timer task run 
2022-10-31 10:05:20 timer task run

範例2:schedule在指定時間time執行一次task,之後每隔period毫秒執行一次task

System.out.println("啟動於:" + DateUtil.formatNow());
Timer timer = new Timer("timer");
timer.schedule(task, DateUtil.parse("2022-10-31 10:07:00", DateUtil.YYYY_MM_DD_HH24_MM_SS), 2000);

輸出:

啟動於:2022-10-31 10:06:39
2022-10-31 10:07:00 timer task run 
2022-10-31 10:07:02 timer task run 
2022-10-31 10:07:04 timer task run 

固定延時 schedule 和 固定速率 scheduleAtFixedRate 在正常情況下看起來功能基本是一致的,區別在於當任務耗時超出執行時間間隔period,後續任務被延誤時,schedule和scheduleAtFixedRate的處理方式不同,後面介紹。

三、固定延時和固定速率區別(重點)

1. 介紹

由於Timer內部僅維護一個執行緒來執行所有任務,所以當前一個任務耗時過長,可能會導致後一個任務的執行被延誤。

出現任務延誤的情況下,固定延時 schedule和 固定速率 scheduleAtFixedRate 的區別就在於,schedule會順延,而scheduleAtFixedRate會把延誤任務立馬補上。

在網上看到幾個非常恰當的例子,貼上來加深理解。

例1:

暑假到了老師給schedule和scheduleAtFixedRate兩個同學佈置作業。

老師要求學生暑假每天寫2頁,30天后完成作業。

這兩個學生每天按時完成作業,直到第10天,出了意外,兩個學生出去旅遊花了5天時間,這5天時間裡兩個人都沒有做作業。任務被拖延了。

這時候兩個學生採取的策略就不同了:

schedule重新安排了任務時間,旅遊回來的第一天做第11天的任務,第二天做第12天的任務,最後完成任務花了35天。

scheduleAtFixedRate是個守時的學生,她總想按時完成老師的任務,於是在旅遊回來的第一天把之前5天欠下的任務以及第16天當天的任務全部完成了,之後還是按照老師的原安排完成作業,最後完成任務花了30天。

例2:

固定速率就好比你今天加班到很晚,但是到了第二天還必須準點到公司上班,如果你一不小心加班到了第二天早上 9 點,你就連休息的時間都沒有了。

而固定時延的意思是你必須睡夠 8 個小時再過來上班,如果你加班到凌晨 6 點,那就可以下午過來上班了。

固定速率強調準點,固定時延強調間隔。

如果任務必須每天準點排程,那就應該使用固定速率排程,並且要確保每個任務執行時間不要太長,避免超過period間隔。

如果任務需要每隔幾分鐘跑一次,那就使用固定時延排程,它不是很在乎單個任務要跑多長時間。

我們來模擬一下這個情況。

首先,我們對TimerTask進行修改,讓它某一次任務產生大量耗時:

TimerTask task = new TimerTask() {
    private int i = 1;
    @Override
    public void run() {
        System.out.print(i + " " + DateUtil.formatNow() + " 開始執行, ");
        if(i == 3) {
            ThreadUtil.sleep(11 * 1000);
        }
        System.out.println(DateUtil.formatNow() + " 結束");
        i++;
    }
};

該任務在執行第3次時,將會休眠11秒,這將會導致延誤後續的任務。

2. 固定速率

範例:

Timer timer = new Timer("timer");
timer.scheduleAtFixedRate(task, 5000, 2000);

設定任務延遲5秒後執行第1次任務,之後每2秒執行一次。

輸出:

啟動於:2022-10-31 15:51:24
1 2022-10-31 15:51:29 開始執行, 2022-10-31 15:51:29 結束
2 2022-10-31 15:51:31 開始執行, 2022-10-31 15:51:31 結束
3 2022-10-31 15:51:33 開始執行, 2022-10-31 15:51:44 結束 *
4 2022-10-31 15:51:44 開始執行, 2022-10-31 15:51:44 結束 *
5 2022-10-31 15:51:44 開始執行, 2022-10-31 15:51:44 結束 *
6 2022-10-31 15:51:44 開始執行, 2022-10-31 15:51:44 結束 *
7 2022-10-31 15:51:44 開始執行, 2022-10-31 15:51:44 結束 *
8 2022-10-31 15:51:44 開始執行, 2022-10-31 15:51:44 結束 *
9 2022-10-31 15:51:45 開始執行, 2022-10-31 15:51:45 結束
10 2022-10-31 15:51:47 開始執行, 2022-10-31 15:51:47 結束
11 2022-10-31 15:51:49 開始執行, 2022-10-31 15:51:49 結束

如果不存在第3次耗時11秒的情況下,正常任務執行時間應該為:

啟動於:2022-10-31 15:51:24
1 2022-10-31 15:51:29 開始執行, 2022-10-31 15:51:29 結束
2 2022-10-31 15:51:31 開始執行, 2022-10-31 15:51:31 結束
3 2022-10-31 15:51:33 開始執行, 2022-10-31 15:51:33 結束 *
4 2022-10-31 15:51:35 開始執行, 2022-10-31 15:51:35 結束 *
5 2022-10-31 15:51:37 開始執行, 2022-10-31 15:51:37 結束 *
6 2022-10-31 15:51:39 開始執行, 2022-10-31 15:51:39 結束 *
7 2022-10-31 15:51:41 開始執行, 2022-10-31 15:51:41 結束 *
8 2022-10-31 15:51:43 開始執行, 2022-10-31 15:51:43 結束 *
9 2022-10-31 15:51:45 開始執行, 2022-10-31 15:51:45 結束
10 2022-10-31 15:51:47 開始執行, 2022-10-31 15:51:47 結束
11 2022-10-31 15:51:49 開始執行, 2022-10-31 15:51:49 結束

但是在第3次執行任務時因為執行耗時11秒,第4次本該在15:51:35開始執行並完成任務,卻到了15:51:44才執行完成,這11秒延誤了後續5個任務的正常執行,因此在15:51:44時,scheduleAtFixedRate趕作業把延誤的5個任務一起執行了。

最後趕上了原本的進度,第9個任務準時在15:51:45執行。

3. 固定延時

範例:

Timer timer = new Timer("timer");
timer.schedule(task, 5000, 2000);

輸出:

啟動於:2022-10-31 15:56:59
1 2022-10-31 15:57:04 開始執行, 2022-10-31 15:57:04 結束
2 2022-10-31 15:57:06 開始執行, 2022-10-31 15:57:06 結束
3 2022-10-31 15:57:08 開始執行, 2022-10-31 15:57:19 結束 *
4 2022-10-31 15:57:19 開始執行, 2022-10-31 15:57:19 結束
5 2022-10-31 15:57:21 開始執行, 2022-10-31 15:57:21 結束
6 2022-10-31 15:57:24 開始執行, 2022-10-31 15:57:24 結束
7 2022-10-31 15:57:26 開始執行, 2022-10-31 15:57:26 結束
8 2022-10-31 15:57:28 開始執行, 2022-10-31 15:57:28 結束
9 2022-10-31 15:57:30 開始執行, 2022-10-31 15:57:30 結束
10 2022-10-31 15:57:32 開始執行, 2022-10-31 15:57:32 結束

如果不存在第3次耗時11秒的情況下,正常任務執行時間應該為:

啟動於:2022-10-31 15:56:59
1 2022-10-31 15:57:04 開始執行, 2022-10-31 15:57:04 結束
2 2022-10-31 15:57:06 開始執行, 2022-10-31 15:57:06 結束
3 2022-10-31 15:57:08 開始執行, 2022-10-31 15:57:08 結束 *
4 2022-10-31 15:57:10 開始執行, 2022-10-31 15:57:10 結束
5 2022-10-31 15:57:12 開始執行, 2022-10-31 15:57:12 結束
6 2022-10-31 15:57:14 開始執行, 2022-10-31 15:57:14 結束
7 2022-10-31 15:57:16 開始執行, 2022-10-31 15:57:16 結束
8 2022-10-31 15:57:18 開始執行, 2022-10-31 15:57:18 結束
9 2022-10-31 15:57:20 開始執行, 2022-10-31 15:57:20 結束
10 2022-10-31 15:57:22 開始執行, 2022-10-31 15:57:22 結束

使用schedule排程,第4次任務本該在15:57:10開始執行,但由於耗時11秒直到15:57:19才開始。

而第3次任務實際是在19秒完成, 完成後又在19秒立即執行第4次,中間少了2秒間隔,第4次完成後接著開始2秒一次,變為了從21秒開始執行第5次。

和我原本的推測不一樣的是,本以為19秒完成後,第4次會隔2秒在21秒執行,沒想到19秒會立即執行。

猜測與delay引數有關,但調整了delay後仍然一樣,完成的那一秒還是會馬上再執行第4次任務。

通過以上測試對比,我們可以感受到Timer中固定速率和固定延時的區別,但為了避免出錯,使用Timer時應讓TimerTask耗時儘可能短。

4. 其他要點

  1. 以上是僅第3次任務加上了耗時11秒,如果是所有任務都耗時11秒呢?

如果每次任務執行都耗時11秒,那麼無論是固定速率還是固定延時,都將是11秒執行一個任務。

  1. 如果改為schedule(TimerTask task, Date firstTime, long period)和scheduleAtFixedRate(TimerTask task, Date firstTime, long period)來排程任務,firstTime指定為10點,而當前系統時間為11點,會出現什麼情況呢?

雖然firstTime已經過期,但是Timer將會立即開始執行任務,之後按照period間隔重複執行任務。

  1. 如果TimerTask執行過程中丟擲了異常會發生什麼事情?

Timer內部僅維護一個執行緒,當任一TimerTask丟擲異常,將導致此執行緒終止執行,該Timer負責的所有任務都無法執行。

四、排程多個TimerTask

在上一節中,介紹的是一個可重複執行的TimeTask,如果執行耗時大於設定的間隔period,將會影響該TimerTask下一次執行的時間點。

而這一節則是為了單獨說明,一個Timer同時排程多個TimeTask也會互相影響。

範例:

TimerTask task1 = new TimerTask() {
    private int i = 1;
    @Override
    public void run() {
        System.out.print(i + " task1:" + DateUtil.formatNow() + " 開始執行, ");
        ThreadUtil.sleep(11 * 1000);
        System.out.println(DateUtil.formatNow() + " 結束");
        i++;
    }
};
TimerTask task2 = new TimerTask() {
    private int i = 1;
    @Override
    public void run() {
        System.out.print(i + "  task2:" + DateUtil.formatNow() + " 開始執行, ");
        ThreadUtil.sleep(11 * 1000);
        System.out.println(DateUtil.formatNow() + " 結束");
        i++;
    }
};

Timer timer = new Timer("timer");
timer.scheduleAtFixedRate(task1, 5000, 2000);
timer.scheduleAtFixedRate(task2, 5000, 2000);

輸出:

1 task1:2022-10-31 16:58:27 開始執行, 2022-10-31 16:58:38 結束
1 task2:2022-10-31 16:58:38 開始執行, 2022-10-31 16:58:49 結束
2 task2:2022-10-31 16:58:49 開始執行, 2022-10-31 16:59:00 結束
2 task1:2022-10-31 16:59:00 開始執行, 2022-10-31 16:59:11 結束
3 task1:2022-10-31 16:59:11 開始執行, 2022-10-31 16:59:22 結束
3 task2:2022-10-31 16:59:22 開始執行, 2022-10-31 16:59:33 結束
4 task2:2022-10-31 16:59:33 開始執行, 2022-10-31 16:59:44 結束
4 task1:2022-10-31 16:59:44 開始執行, 2022-10-31 16:59:55 結束

可以發現,task1和task2其實都沒有按照既定時間去執行任務了。

根本原因是在於,Timer內部僅維護一個執行緒執行所有TimerTask,為了避免錯誤,一個Timer物件最好僅排程一個TimerTask物件,除非可以確保多個TimerTask之間一定不會相互影響。

因此編寫TimerTask時應當自行捕獲異常。

五、取消任務

Timer在建立時實際上是預設在內部維護了一個非守護執行緒,即使任務全部執行完成,執行緒也並不會銷燬。

Timer提供cancel()方法,可以手動呼叫取消定時器所有的任務,並銷燬定時器。

如果想要Timer內部建立的是守護執行緒,可以使用以下構造方法建立定時器,設定isDaemon為true:

  • Timer(boolean isDaemon)
  • Timer(String name, boolean isDaemon)

如果沒有自己定義name引數,預設Timer內部自動命名為「Timer-遞增序號」,作為內部執行緒的執行緒名稱,在構造方法內啟動此執行緒。

如果是要取消單個任務,可以使用TimerTask的cancel()方法。

當TimerTask呼叫cancel之後,任務是取消了,但Timer自身並不能馬上知道TimerTask被取消,而是在準備執行前才知道,因此Timer內部還維護著這個任務的參照。若希望Timer立即清除參照,可呼叫Timer.purge()立即執行清除。