java.util包下提供了對定時任務的支援,涉及2個類:
使用該定時任務我們需要繼承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個方法,如下:
可重複執行任務是指,任務允許按照設定的規則重複執行。
可重複執行任務共有4個方法,分為 固定延時 schedule 和 固定速率 scheduleAtFixedRate:
範例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的處理方式不同,後面介紹。
由於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秒,這將會導致延誤後續的任務。
範例:
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執行。
範例:
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耗時儘可能短。
如果每次任務執行都耗時11秒,那麼無論是固定速率還是固定延時,都將是11秒執行一個任務。
雖然firstTime已經過期,但是Timer將會立即開始執行任務,之後按照period間隔重複執行任務。
Timer內部僅維護一個執行緒,當任一TimerTask丟擲異常,將導致此執行緒終止執行,該Timer負責的所有任務都無法執行。
在上一節中,介紹的是一個可重複執行的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:
如果沒有自己定義name引數,預設Timer內部自動命名為「Timer-遞增序號」,作為內部執行緒的執行緒名稱,在構造方法內啟動此執行緒。
如果是要取消單個任務,可以使用TimerTask的cancel()方法。
當TimerTask呼叫cancel之後,任務是取消了,但Timer自身並不能馬上知道TimerTask被取消,而是在準備執行前才知道,因此Timer內部還維護著這個任務的參照。若希望Timer立即清除參照,可呼叫Timer.purge()立即執行清除。