本節,我們來探討Java中的定時任務Timer和ScheduledExecutorService,它們的基本用法都是比較簡單的,但如果對它們缺乏足夠的瞭解,則很容易墜入其中的一些坑,都有哪些坑呢? ...
本節探討定時任務,定時任務的應用場景是非常多的,比如:
- 鬧鐘程式或任務提醒,指定時間叫床或在指定日期提醒還信用卡
- 監控系統,每隔一段時間採集下系統數據,對異常事件報警
- 統計系統,一般凌晨一定時間統計昨日的各種數據指標
在Java中,有兩種方式實現定時任務:
- 使用java.util包中的Timer和TimerTask
- 使用Java併發包中的ScheduledExecutorService
它們的基本用法都是比較簡單的,但如果對它們沒有足夠的瞭解,則很容易陷入其中的一些陷阱,下麵,我們就來介紹它們的用法、原理以及那些坑。
Timer和TimerTask
基本用法
TimerTask表示一個定時任務,它是一個抽象類,實現了Runnable,具體的定時任務需要繼承該類,實現run方法。
Timer是一個具體類,它負責定時任務的調度和執行,它有如下主要方法:
//在指定絕對時間time運行任務task public void schedule(TimerTask task, Date time) //在當前時間延時delay毫秒後運行任務task public void schedule(TimerTask task, long delay) //固定延時重覆執行,第一次計劃執行時間為firstTime,後一次的計劃執行時間為前一次"實際"執行時間加上period public void schedule(TimerTask task, Date firstTime, long period) //同樣是固定延時重覆執行,第一次執行時間為當前時間加上delay public void schedule(TimerTask task, long delay, long period) //固定頻率重覆執行,第一次計劃執行時間為firstTime,後一次的計劃執行時間為前一次"計劃"執行時間加上period public void scheduleAtFixedRate(TimerTask task, Date firstTime, long period) //同樣是固定頻率重覆執行,第一次計劃執行時間為當前時間加上delay public void scheduleAtFixedRate(TimerTask task, long delay, long period)
需要註意固定延時(fixed-delay)與固定頻率(fixed-rate)的區別,都是重覆執行,但後一次任務執行相對的時間是不一樣的,對於固定延時,它是基於上次任務的"實際"執行時間來算的,如果由於某種原因,上次任務延時了,則本次任務也會延時,而固定頻率會儘量補夠運行次數。
另外,需要註意的是,如果第一次計劃執行的時間firstTime是一個過去的時間,則任務會立即運行,對於固定延時的任務,下次任務會基於第一次執行時間計算,而對於固定頻率的任務,則會從firstTime開始算,有可能加上period後還是一個過去時間,從而連續運行很多次,直到時間超過當前時間。
我們通過一些簡單的例子具體來看下。
基本示例
看一個最簡單的例子:
public class BasicTimer { static class DelayTask extends TimerTask { @Override public void run() { System.out.println("delayed task"); } } public static void main(String[] args) throws InterruptedException { Timer timer = new Timer(); timer.schedule(new DelayTask(), 1000); Thread.sleep(2000); timer.cancel(); } }
創建一個Timer對象,1秒鐘後運行DelayTask,最後調用Timer的cancel方法取消所有定時任務。
看一個固定延時的簡單例子:
public class TimerFixedDelay { static class LongRunningTask extends TimerTask { @Override public void run() { try { Thread.sleep(5000); } catch (InterruptedException e) { } System.out.println("long running finished"); } } static class FixedDelayTask extends TimerTask { @Override public void run() { System.out.println(System.currentTimeMillis()); } } public static void main(String[] args) throws InterruptedException { Timer timer = new Timer(); timer.schedule(new LongRunningTask(), 10); timer.schedule(new FixedDelayTask(), 100, 1000); } }
有兩個定時任務,第一個運行一次,但耗時5秒,第二個是重覆執行,1秒一次,第一個先運行。運行該程式,會發現,第二個任務只有在第一個任務運行結束後才會開始運行,運行後1秒一次。
如果替換上面的代碼為固定頻率,即代碼變為:
public class TimerFixedRate { static class LongRunningTask extends TimerTask { @Override public void run() { try { Thread.sleep(5000); } catch (InterruptedException e) { } System.out.println("long running finished"); } } static class FixedRateTask extends TimerTask { @Override public void run() { System.out.println(System.currentTimeMillis()); } } public static void main(String[] args) throws InterruptedException { Timer timer = new Timer(); timer.schedule(new LongRunningTask(), 10); timer.scheduleAtFixedRate(new FixedRateTask(), 100, 1000); } }
運行該程式,第二個任務同樣只有在第一個任務運行結束後才會運行,但它會把之前沒有運行的次數補過來,一下子運行5次,輸出類似下麵這樣:
long running finished 1489467662330 1489467662330 1489467662330 1489467662330 1489467662330 1489467662419 1489467663418
基本原理
Timer內部主要由兩部分組成,任務隊列和Timer線程。任務隊列是一個基於堆實現的優先順序隊列,按照下次執行的時間排優先順序。Timer線程負責執行所有的定時任務,需要強調的是,一個Timer對象只有一個Timer線程,所以,對於上面的例子,任務才會被延遲。
Timer線程主體是一個迴圈,從隊列中拿任務,如果隊列中有任務且計劃執行時間小於等於當前時間,就執行它,如果隊列中沒有任務或第一個任務延時還沒到,就睡眠。如果睡眠過程中隊列上添加了新任務且新任務是第一個任務,Timer線程會被喚醒,重新進行檢查。
在執行任務之前,Timer線程判斷任務是否為周期任務,如果是,就設置下次執行的時間並添加到優先順序隊列中,對於固定延時的任務,下次執行時間為當前時間加上period,對於固定頻率的任務,下次執行時間為上次計劃執行時間加上period。
需要強調是,下次任務的計劃是在執行當前任務之前就做出了的,對於固定延時的任務,延時相對的是任務執行前的當前時間,而不是任務執行後,這與後面講到的ScheduledExecutorService的固定延時計算方法是不同的,後者的計算方法更合乎一般的期望。
另一方面,對於固定頻率的任務,它總是基於最先的計劃計劃的,所以,很有可能會出現前面例子中一下子執行很多次任務的情況。
死迴圈
一個Timer對象只有一個Timer線程,這意味著,定時任務不能耗時太長,更不能是無限迴圈,看個例子:
public class EndlessLoopTimer { static class LoopTask extends TimerTask { @Override public void run() { while (true) { try { // ... 執行任務 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } // 永遠也沒有機會執行 static class ExampleTask extends TimerTask { @Override public void run() { System.out.println("hello"); } } public static void main(String[] args) throws InterruptedException { Timer timer = new Timer(); timer.schedule(new LoopTask(), 10); timer.schedule(new ExampleTask(), 100); } }
第一個定時任務是一個無限迴圈,其後的定時任務ExampleTask將永遠沒有機會執行。
異常處理
關於Timer線程,還需要強調非常重要的一點,在執行任何一個任務的run方法時,一旦run拋出異常,Timer線程就會退出,從而所有定時任務都會被取消。我們看個簡單的示例:
public class TimerException { static class TaskA extends TimerTask { @Override public void run() { System.out.println("task A"); } } static class TaskB extends TimerTask { @Override public void run() { System.out.println("task B"); throw new RuntimeException(); } } public static void main(String[] args) throws InterruptedException { Timer timer = new Timer(); timer.schedule(new TaskA(), 1, 1000); timer.schedule(new TaskB(), 2000, 1000); } }
期望TaskA每秒執行一次,但TaskB會拋出異常,導致整個定時任務被取消,程式終止,屏幕輸出為:
task A task A task B Exception in thread "Timer-0" java.lang.RuntimeException at laoma.demo.timer.TimerException$TaskB.run(TimerException.java:21) at java.util.TimerThread.mainLoop(Timer.java:555) at java.util.TimerThread.run(Timer.java:505)
所以,如果希望各個定時任務不互相干擾,一定要在run方法內捕獲所有異常。
小結
可以看到,Timer/TimerTask的基本使用是比較簡單的,但我們需要註意:
- 背後只有一個線程在運行
- 固定頻率的任務被延遲後,可能會立即執行多次,將次數補夠
- 固定延時任務的延時相對的是任務執行前的時間
- 不要在定時任務中使用無限迴圈
- 一個定時任務的未處理異常會導致所有定時任務被取消
ScheduledExecutorService
介面和類定義
由於Timer/TimerTask的一些問題,Java併發包引入了ScheduledExecutorService,它是一個介面,其定義為:
public interface ScheduledExecutorService extends ExecutorService { //單次執行,在指定延時delay後運行command public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit); //單次執行,在指定延時delay後運行callable public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit); //固定頻率重覆執行 public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit); //固定延時重覆執行 public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit); }
它們的返回類型都是ScheduledFuture,它是一個介面,擴展了Future和Delayed,沒有定義額外方法。這些方法的大部分語義與Timer中的基本是類似的。對於固定頻率的任務,第一次執行時間為initialDelay後,第二次為initialDelay+period,第三次initialDelay+2*period,依次類推。不過,對於固定延時的任務,它是從任務執行後開始算的,第一次為initialDelay後,第二次為第一次任務執行結束後再加上delay。與Timer不同,它不支持以絕對時間作為首次運行的時間。
ScheduledExecutorService的主要實現類是ScheduledThreadPoolExecutor,它是線程池ThreadPoolExecutor的子類,是基於線程池實現的,它的主要構造方法是:
public ScheduledThreadPoolExecutor(int corePoolSize)
此外,還有構造方法可以接受參數ThreadFactory和RejectedExecutionHandler,含義與ThreadPoolExecutor一樣,我們就不贅述了。
它的任務隊列是一個無界的優先順序隊列,所以最大線程數對它沒有作用,即使corePoolSize設為0,它也會至少運行一個線程。
工廠類Executors也提供了一些方便的方法,以方便創建ScheduledThreadPoolExecutor,如下所示:
//單線程的定時任務執行服務 public static ScheduledExecutorService newSingleThreadScheduledExecutor() public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) //多線程的定時任務執行服務 public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory)
基本示例
由於可以有多個線程執行定時任務,一般任務就不會被某個長時間運行的任務所延遲了,比如,對於前面的TimerFixedDelay,如果改為:
public class ScheduledFixedDelay { static class LongRunningTask implements Runnable { @Override public void run() { try { Thread.sleep(5000); } catch (InterruptedException e) { } System.out.println("long running finished"); } } static class FixedDelayTask implements Runnable { @Override public void run() { System.out.println(System.currentTimeMillis()); } } public static void main(String[] args) throws InterruptedException { ScheduledExecutorService timer = Executors.newScheduledThreadPool(10); timer.schedule(new LongRunningTask(), 10, TimeUnit.MILLISECONDS); timer.scheduleWithFixedDelay(new FixedDelayTask(), 100, 1000, TimeUnit.MILLISECONDS); } }
再次執行,第二個任務就不會被第一個任務延遲了。
另外,與Timer不同,單個定時任務的異常不會再導致整個定時任務被取消了,即使背後只有一個線程執行任務,我們看個例子:
public class ScheduledException { static class TaskA implements Runnable { @Override public void run() { System.out.println("task A"); } } static class TaskB implements Runnable { @Override public void run() { System.out.println("task B"); throw new RuntimeException(); } } public static void main(String[] args) throws InterruptedException { ScheduledExecutorService timer = Executors .newSingleThreadScheduledExecutor(); timer.scheduleWithFixedDelay(new TaskA(), 0, 1, TimeUnit.SECONDS); timer.scheduleWithFixedDelay(new TaskB(), 2, 1, TimeUnit.SECONDS); } }
TaskA和TaskB都是每秒執行一次,TaskB兩秒後執行,但一執行就拋出異常,屏幕的輸出類似如下:
task A
task A
task B
task A
task A
...
這說明,定時任務TaskB被取消了,但TaskA不受影響,即使它們是由同一個線程執行的。不過,需要強調的是,與Timer不同,沒有異常被拋出來,TaskB的異常沒有在任何地方體現。所以,與Timer中的任務類似,應該捕獲所有異常。
基本原理
ScheduledThreadPoolExecutor的實現思路與Timer基本是類似的,都有一個基於堆的優先順序隊列,保存待執行的定時任務,它的主要不同是:
- 它的背後是線程池,可以有多個線程執行任務
- 它在任務執行後再設置下次執行的時間,對於固定延時的任務更為合理
- 任務執行線程會捕獲任務執行過程中的所有異常,一個定時任務的異常不會影響其他定時任務,但發生異常的任務也不再被重新調度,即使它是一個重覆任務
小結
本節介紹了Java中定時任務的兩種實現方式,Timer和ScheduledExecutorService,需要特別註意Timer的一些陷阱,實踐中建議使用ScheduledExecutorService。
它們的共同局限是,不太勝任複雜的定時任務調度,比如,每周一和周三晚上18:00到22:00,每半小時執行一次。對於類似這種需求,可以利用我們之前在32節和33節介紹的日期和時間處理方法,或者利用更為強大的第三方類庫,比如Quartz(http://www.quartz-scheduler.org/)。
在併發應用程式中,一般我們應該儘量利用高層次的服務,比如前面章節介紹的各種併發容器、任務執行服務和線程池等,避免自己管理線程和它們之間的同步,但在個別情況下,自己管理線程及同步是必需的,這時,除了利用前面章節介紹的synchronized, wait/notify, 顯示鎖和條件等基本工具,Java併發包還提供了一些高級的同步和協作工具,以方便實現併發應用,讓我們下一節來瞭解它們。
(與其他章節一樣,本節所有代碼位於 https://github.com/swiftma/program-logic)
----------------
未完待續,查看最新文章,敬請關註微信公眾號“老馬說編程”(掃描下方二維碼),從入門到高級,深入淺出,老馬和你一起探索Java編程及電腦技術的本質。用心原創,保留所有版權。