定時任務就是在指定時間執行程序,或周期性執行計划任務。Java中實現定時任務的方法有很多,本文從從JDK自帶的一些方法來實現定時任務的需求。
Timer和TimerTask可以作為線程實現的第三種方式(前兩種詳見《Java多線程基礎》),JDK1.5之后定時任務推薦使用ScheduledThreadPoolExecutor。
1、快速入門
Timer運行在后台,可以執行任務一次,或定期執行任務。TimerTask類繼承了Runnable接口,因此具備多線程的能力。一個Timer可以調度任意多個TimerTask,所有任務都存儲在一個隊列中順序執行,如果需要多個TimerTask並發執行,則需要創建兩個多個Timer。
一個簡單使用Timer的例子如下:
import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Timer; import java.util.TimerTask; public class TimerTest { //被執行的任務必須繼承TimerTask,並且實現run方法 static class MyTimerTask1 extends TimerTask { public void run() { System.out.println("爆炸!!!"); } } public static void main(String[] args) throws ParseException { Timer timer = new Timer(); //1、設定兩秒后執行任務 //timer.scheduleAtFixedRate(new MyTimerTask1(), 2000,1000); //2、設定任務在執行時間執行,本例設定時間13:57:00 SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); Date time = dateFormatter.parse("2014/02/11 14:40:00"); timer.schedule(new MyTimerTask1(), time); } }
2、schedule與scheduleAtFixedRate使用方法
schedule(TimerTask task, long delay, long period) --指定任務執行延遲時間
schedule(TimerTask task, Date time, long period) --指定任務執行時刻
scheduleAtFixedRate(TimerTask task, long delay, long period)
scheduleAtFixedRate(TimerTask task, Date firstTime, long period)
3、schedule與scheduleAtFixedRate區別
1) schedule:
① 注重任務執行的平滑度,也就是說任務隊列中某個任務執行延遲了某個時間,接下來的其余任務都會延遲相同時間,來最大限度的保證任務與任務之間的時間間隔的完整性;
② 當程序指定開始時刻(Date time)小於當前系統時刻時,會立即執行一次任務,之后的任務開始執行時間以當前時刻為標准,結合時間間隔計算得到;
例:計划任務程序指定從2014/02/11 18:00:00開始每隔3分鍾執行一次任務。如果該程序在18:00:00之前運行,則計划任務程序分別會在18:00:00、18:03:00、18:06:00...等時間點執行任務;如果該程序在18:00:00之后運行,如在18:07:00時刻開始運行程序,計划任務程序判斷指定開始執行時刻18:00:00小於當前系統時刻,於是立即執行一次任務,接下來任務時間時刻分別為18:10:00、18:13:00、18:16:00...;而當使用scheduleAtFixedRate執行計划任務時,無論計划任務程序在什么時候運行,所有任務執行的次數都按照原計划,不會因為程序執行時刻的早晚而改變。而當程序運行時刻比計划任務計划首次執行時間晚時,如同樣在18:07:00時刻開始執行程序,則計划任務程序會立馬計算程序執行時刻晚於指定時刻,會立即執行(18:07:00-18:00:00)/3+1=3次任務(代表18:00:00、18:03:00和18:06:00三個時刻執行的任務),接下來任務執行時刻是18:09:00、18:12:00等。
import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Timer; import java.util.TimerTask; public class TimerRateFix { public static void main(String[] args) throws ParseException { final SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); Date startDate = dateFormatter.parse("2014/02/11 18:00:00"); Timer timer = new Timer(); timer.schedule(new TimerTask(){ public void run() { System.out.println("執行任務,當前時刻:" + dateFormatter.format(new Date())); } },startDate,3*60*1000); } }
③ 當執行任務的時間間隔t1大於周期間隔t2時,下一次任務執行時間點相對於上一次任務實際執行完成的時間點,每個任務的執行時間會延后,第n個計划任務的實際執行時間比預計要延后(t1-t2)*n個時間單位。
例:計划任務程序指定從2014/02/11 18:00:00開始每隔5秒執行一次任務,每次任務執行時間為6秒。當程序在18:00:00之前執行時,schedule分別會在18:00:00、18:00:06、18:00:12...等時間點執行計划任務,每隔時間點間隔6秒。原因是根據計划,第一個計划任務應會在18:00:00執行,第二個計划任務應會在18:00:05執行,而在18:00:05時間點,第一個任務才執行了5秒,還需要1秒才執行結束,因此第二個任務不能執行,於是等待1秒后在18:00:06時刻執行,之后每個任務均如此,均比原定執行時刻有延遲,每個任務時間間隔為6秒。當使用scheduleAtFixedRate執行計划任務時,第一個計划任務在18:00:00時刻執行,第二個會根據計划在18:00:05執行,第三個會在18:00:10執行,每個任務執行時間間隔為5秒,詳細執行情況如下圖所示
圖1 schedule與scheduleAtFixedRate任務執行區別
import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Timer; import java.util.TimerTask; public class TimerRateTest { public static void main(String[] args) throws ParseException { final SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); Timer timer = new Timer(); Date time = dateFormatter.parse("2014/02/11 18:00:00"); //假設程序在2014/02/11 18:00:00之前啟動 //1、使用scheduleAtFixedRate,每個計划任務執行時間點嚴格為18:00:00、18:00:05、18:00:10...,當任務執行時間大於時間間隔時可能會有並發情況 //2、使用schedule,每個計划任務執行時間點根據上一個任務執行結束時間及時間間隔來計算 // 當任務執行時間t1>時間間隔t2時,第N個計划任務執行時間點延遲為(t1-t2)*N,執行時間點為18:00:00+t2*(N-1)+(t1-t2)*N // 當任務執行時間t1<=時間間隔t2時,第N個計划任務執行時間點無延遲,執行時間為原計划 timer.scheduleAtFixedRate(new TimerTask(){ public void run() { try { //每個計划任務執行時間為6秒 Thread.sleep(6000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("結束當前任務,當前時間:"+ dateFormatter.format(new Date())); } },time,5000); //計划任務執行時間間隔為5秒 } }
2) scheduleAtFixedRate:
① 注重任務執行的頻度,也就是說計划任務程序開始執行,每隔任務執行的時間點就已經確定,並不會因為某個任務的延遲而延遲執行其他任務,可以保證任務執行的時間效率;
② 當程序指定開始時刻(Date firstTime)小於當前系統時刻時,會立即執行任務,執行次數為(當前系統時刻-指定開始時刻)/時間間隔,之后的任務開始執行時刻與當前系統時刻無關,仍按照程序指定開始時刻根據時間間隔計算得到;
③ 當執行任務的時間間隔t1大於周期間隔t2時,下一次任務執行時間點還是按照原定計划不變,因此這種情況,有部分時間斷可能有多個任務並發執行;
4、終止Timer線程
1) 調用Timer.cancle()方法。可以在程序任何地方調用,甚至在TimerTask中的run方法中調用;
2) 創建Timer時定義位daemon守護線程(有關守護線程見《Java守護線程》),使用new Timer(true)語句;
3) 設置Timer對象為null,其會自動終止;
4) 調用System.exit方法,整個程序終止。
5、Timer線程的缺點
1) Timer線程不會捕獲異常,所以TimerTask拋出的未檢查的異常會終止timer線程。如果Timer線程中存在多個計划任務,其中一個計划任務拋出未檢查的異常,則會引起整個Timer線程結束,從而導致其他計划任務無法得到繼續執行。
2) Timer線程時基於絕對時間(如:2014/02/14 16:06:00),因此計划任務對系統的時間的改變是敏感的。
3) Timer是單線程,如果某個任務很耗時,可能會影響其他計划任務的執行。
因此,JDK1.5以上建議使用ScheduledThreadPoolExecutor來代替Timer執行計划任務。
ScheduledThreadPoolExecutor是JDK1.5以后推出的類,用於實現定時、重復執行的功能,官方文檔解釋要優於Timer。
1、構造方法
1) ScheduledThreadPoolExecutor(int corePoolSize) 使用給定核心池大小創建一個新定定時線程池
2) ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactorythreadFactory) 使用給定的初始參數創建一個新對象,可提供線程創建工廠
private final static ScheduledThreadPoolExecutor schedual = new ScheduledThreadPoolExecutor(1, new ThreadFactory() { private AtomicInteger atoInteger = new AtomicInteger(0); public Thread newThread(Runnable r) { Thread t = new Thread(r); t.setName("xxx-Thread "+ atoInteger.getAndIncrement()); return t; } });
2、調度方法
1) schedule(Callable callable, long delay, TimeUnit unit); 延遲delay時間后開始執行callable
2) scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit); 延遲initialDelay時間后開始執行command,並且按照period時間周期性重復調用,當任務執行時間大於間隔時間時,之后的任務都會延遲,此時與Timer中的schedule方法類似
3) scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit); 延遲initialDelay時間后開始執行command,並且按照period時間周期性重復調用,這里的間隔時間delay是等上一個任務完全執行完畢才開始計算,與Timer中scheduleAtFixedRate情況不同。

圖2 ScheduledThreadPoolExecutor.scheduleWithFixedDelay與Timer.scheduleAtFixedRate任務執行區別
3、與Timer相比,優點有:
1) ScheduledThreadPoolExecutor線程會捕獲任務重的異常,即使多個計划任務中存在某幾個計划任務為捕獲異常的情況,也不會影響ScheduledThreadPoolExecutor總線程的工作,不會影響其他計划任務的繼續執行。
2) ScheduledThreadPoolExecutor是基於相對時間的,對系統時間的改變不敏感,但是如果執行某一絕對時間(如2014/02/14 17:13:06)執行任務,可能不好執行,此時可使用Timer。
3) ScheduledThreadPoolExecutor是線程池,如任務數過多或某些任務執行時間較長,可自動分配更多的線程來執行計划任務。
總之,JDK1.5之后,計划任務建議使用ScheduledThreadPoolExecutor。
