2011年12月25日

Java Timer:schedule和scheduleAtFixedRate有何不同


 
在前一篇文章 Java Timer:排程、定時、週期性執行工作任務 中,
我們展示了如何利用 Timer 非常簡單地執行排程工作。
然而在 Timer 中有兩個用來排程的 method:schedule 和 scheduleAtFixedRate,
兩者最明顯的差異就是字面上的不同,後者多了 AtFixedRate,
可是在上一篇我們用 schedule 也可以重複執行工作啊?
到底這兩者差在哪裡呢?使用時機又該如何決定呢?
圖片來源:http://jeequ.com/12016/

為了解決這個問題,我直接查了 Timer 的 source code,
其中最關鍵的差異為:
  1. schedule為「fixed-delay」,執行時間參照前次工作執行完成的時間:
    若執行工作沒被 delay,則按照預訂的時間執行;但若執行工作時被 delay了,
    随後工作的預訂執行時間會按照上一次執行「完成」的時間點來計算
  2. scheduleAtFixedRate為「fixed-rate」,執行時間參照一開始的時間點;
    和schedule一樣,若執行工作沒被 delay,則按照預訂的時間執行;
    但如果執行工作時被delay了,
    後續的預訂執行時間仍按照上一次執行「開始」的時間點計算,
    為了「catch up」預訂時間,會連續多次補足未完成的任務
另外在在單次執行的情況下,
只要預訂要執行的時間已經過了,那麼兩者都會直接把工作移除不執行。
但若在有設定 period 的情況下,若預訂要執行的時間已經過了,
基於上面所描述的行為,schedule 沒有上次執行完成的時間,會從現在開始算並執行,
而 scheduleAtFixedRate 則以預訂執行的時間開始算,且會一口氣將過去未做的補上!

接著讓我們來看例子,首先我們要看的是當執行所需時間比執行間隔長和短時,
兩者的運作會有何差異:
下面是工作 Task 的程式碼,每次進到工作時會 random 睡上 4~8秒才結束。
 1 package werdna1222coldcodes.blogspot.com.demo.timer;
 2 
 3 import java.util.*;
 4 
 5 public class DateTaskSleep4to8s extends TimerTask {
 6     public void run() {
 7         System.out.println("Task 預訂執行時間:" 
 8                 + new Date(this.scheduledExecutionTime()) 
 9                 + ", \n實際執行時間:" + new Date());
10         try {
11             int sleepSeconds = (int) (4 + Math.random()*4);
12             System.out.println(
13                 "Task going to sleep " + sleepSeconds + "s.");
14             Thread.sleep(sleepSeconds*1000);
15         }
16             catch(InterruptedException e) {
17         }
18     }
19 }
下面則是測試程式的程式碼,預訂的時間間隔為 5秒:
 1 package werdna1222coldcodes.blogspot.com.demo.timer;
 2 
 3 import java.util.*;
 4 
 5 public class TimerScheduleAndScheduleAtFixedRateDemo {
 6     public static void main(String[] args) {
 7 
 8         TimerScheduleAndScheduleAtFixedRateDemo timerDemo 
 9             = new TimerScheduleAndScheduleAtFixedRateDemo();
10         timerDemo.testSchedule();
11         timerDemo.testScheduleAtFixedRate();
12     }
13     
14     void testSchedule(){
15 
16         Timer timer = new Timer();
17         System.out.println("In testSchedule:" + new Date());
18         System.out.println("Delay:5秒, Period:5秒");
19         
20         // schedule(TimerTask task, long delay, long period)
21         timer.schedule(new DateTaskSleep4to8s(), new Date(), 5000);
22         
23         try {
24             Thread.sleep(30000);
25         }
26             catch(InterruptedException e) {
27         }
28 
29         timer.cancel();
30         System.out.println("End testSchedule:" + new Date() + "\n");
31     }
32     
33     void testScheduleAtFixedRate(){
34 
35         Timer timer = new Timer();
36         System.out.println("In testScheduleAtFixedRate:" + new Date()
37                            );
38         System.out.println("Delay:5秒, Period:5秒");
39         
40         // scheduleAtFixedRate(TimerTask task, long delay, long period)
41         timer.scheduleAtFixedRate(new DateTaskSleep4to8s(), new Date(),
42                                   5000);
43         
44         try {
45             Thread.sleep(30000);
46         }
47             catch(InterruptedException e) {
48         }
49 
50         timer.cancel();
51         System.out.println("End testScheduleAtFixedRate:" + new Date(
52                            ) + "\n");
53     }
54 }
執行結果:
首先來看 schedule 的部份,由下面的資料我們可以發現,
當工作執行時間超過 5 秒時,下次的預訂執行時間會以工作結束的時間來計算,
如下面標紅色部份,工作執行了 7秒,則下次的預訂時間就晚 7秒,
而若工作時間少於 5 秒,下次執行的間隔仍維持 5 秒,如下面標藍色部份。
對 schedule 而言,所有預訂和實際執行時間都是相同的,沒有 catch up 的情況。
In testSchedule:Sun Dec 25 14:56:31 CST 2011
Delay:5秒, Period:5秒
Task 預訂執行時間:Sun Dec 25 14:56:31 CST 2011, 
實際執行時間:Sun Dec 25 14:56:31 CST 2011
Task going to sleep 5s.
Task 預訂執行時間:Sun Dec 25 14:56:36 CST 2011, 
實際執行時間:Sun Dec 25 14:56:36 CST 2011
Task going to sleep 7s.
Task 預訂執行時間:Sun Dec 25 14:56:43 CST 2011, 
實際執行時間:Sun Dec 25 14:56:43 CST 2011
Task going to sleep 5s.
Task 預訂執行時間:Sun Dec 25 14:56:48 CST 2011, 
實際執行時間:Sun Dec 25 14:56:48 CST 2011
Task going to sleep 6s.
Task 預訂執行時間:Sun Dec 25 14:56:54 CST 2011, 
實際執行時間:Sun Dec 25 14:56:54 CST 2011
Task going to sleep 4s.
Task 預訂執行時間:Sun Dec 25 14:56:59 CST 2011, 
實際執行時間:Sun Dec 25 14:56:59 CST 2011
Task going to sleep 5s.
End testSchedule:Sun Dec 25 14:57:01 CST 2011
而對 scheduleAtFixedRate 來說,
當工作執行時間超過 5 秒時,下次的預訂執行時間仍以工作開始的時間來計算,
所以所有後續工作的預訂時間都是間隔 5秒,然而排程的工作不會同時執行
故雖然預訂時間間隔5秒,但實際執行時間會被 delay,如下面標紅色部份,
工作執行了 6秒,則下次的預訂時間是5秒後,但實際執行是6秒後。
這個預訂時間和實際執行時間的差距,將在後續的工作排程中產生影響,
在實際執行時間落後預訂時間時,scheduleAtFixedRate 會有 catch up 的機制,
在後續若執行時間較短,我們就可以發現執行的間距小於 5秒,如藍色所示。
In testScheduleAtFixedRate:Sun Dec 25 14:57:01 CST 2011
Delay:5秒, Period:5秒
Task 預訂執行時間:Sun Dec 25 14:57:01 CST 2011, 
實際執行時間:Sun Dec 25 14:57:01 CST 2011
Task going to sleep 6s.
Task 預訂執行時間:Sun Dec 25 14:57:06 CST 2011, 
實際執行時間:Sun Dec 25 14:57:07 CST 2011
Task going to sleep 5s.
Task 預訂執行時間:Sun Dec 25 14:57:11 CST 2011, 
實際執行時間:Sun Dec 25 14:57:12 CST 2011
Task going to sleep 5s.
Task 預訂執行時間:Sun Dec 25 14:57:16 CST 2011, 
實際執行時間:Sun Dec 25 14:57:17 CST 2011
Task going to sleep 6s.
Task 預訂執行時間:Sun Dec 25 14:57:21 CST 2011, 
實際執行時間:Sun Dec 25 14:57:23 CST 2011
Task going to sleep 4s.
Task 預訂執行時間:Sun Dec 25 14:57:26 CST 2011, 
實際執行時間:Sun Dec 25 14:57:27 CST 2011
Task going to sleep 6s.
End testScheduleAtFixedRate:Sun Dec 25 14:57:31 CST 2011
看完了以上的例子,我們發現 schedule 和 scheduleAtFixedRate都不會同時執行,
而是接續著執行,所以不須考慮同步的問題。
這點在 Timer的schedule和scheduleAtFixedRate方法的区别解析 的解釋是錯的,
因為他印出的都是 scheduledExecutionTime,而非實際執行時間。

接下來我們再來看看若預訂執行的時間已經過了,兩個不同 method 有何差異,
這也會展示 catch up 的影響到底有多大:
這次我們不再需要不同的工作時間,所以工作的程式碼簡化如下:
 1 package werdna1222coldcodes.blogspot.com.demo.timer;
 2 
 3 import java.util.*;
 4 
 5 public class DateTaskWithBothTime extends TimerTask {
 6     
 7     @ Override
 8     public void run() {
 9         System.out.println("Task 預訂執行時間:" 
10                 + new Date(this.scheduledExecutionTime()) 
11                 + ", \n實際執行時間:" + new Date());
12     }
13 }
而測試的程式碼如下:
 1 package werdna1222coldcodes.blogspot.com.demo.timer;
 2 
 3 import java.util.*;
 4 
 5 public class TimerScheduleAndScheduleAtFixedRateDemo {
 6     public static void main(String[] args) {
 7 
 8         TimerScheduleAndScheduleAtFixedRateDemo timerDemo 
 9             = new TimerScheduleAndScheduleAtFixedRateDemo();
10 
11         timerDemo.testSchedulePassedDate();
12         timerDemo.testScheduleAtFixedRatePassedDate();
13     }
14     
15     void testSchedulePassedDate(){
16 
17         Timer timer = new Timer();
18         System.out.println("In testSchedulePassedDate:" + new Date())
19                            ;
20         System.out.println("Period:5秒");
21         
22         // 設定填入schedule中的 Date firstTime 
23         為現在的15秒前
24         Calendar calendar = Calendar.getInstance();
25         calendar.set(Calendar.SECOND, calendar.get(Calendar.SECOND)-15)
26                      ;
27         Date firstTime = calendar.getTime();
28         
29         // 也可用 simpleDateFormat 直接設定 firstTime 
30         的精確時間
31         // SimpleDateFormat dateFormatter = 
32         //      new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");  
33         // Date firstTime = dateFormatter.parse("2011/12/25 13:30:00");
34         
35         // schedule(TimerTask task, Date firstTime, long period)
36         timer.schedule(new DateTaskWithBothTime(), firstTime, 5000);
37         
38         try {
39             Thread.sleep(30000);
40         }
41             catch(InterruptedException e) {
42         }
43 
44         timer.cancel();
45         System.out.println("End testSchedulePassedDate:" + new Date()
46                            + "\n");
47     }
48     
49     void testScheduleAtFixedRatePassedDate(){
50 
51         Timer timer = new Timer();
52         System.out.println("In testScheduleAtFixedRatePassedDate:" 
53                 + new Date());
54         System.out.println("Period:5秒");
55         
56         // 設定填入schedule中的 Date firstTime 
57         為現在的15秒前
58         Calendar calendar = Calendar.getInstance();
59         calendar.set(Calendar.SECOND, calendar.get(Calendar.SECOND)-15)
60                      ;
61         Date firstTime = calendar.getTime();
62         
63         // schedule(TimerTask task, Date firstTime, long period)
64         timer.scheduleAtFixedRate(new DateTaskWithBothTime(), 
65                                   firstTime, 5000);
66         
67         try {
68             Thread.sleep(30000);
69         }
70             catch(InterruptedException e) {
71         }
72 
73         timer.cancel();
74         System.out.println("End testScheduleAtFixedRatePassedDate:" 
75                 + new Date() + "\n");
76     }
77  }
執行的結果為:
In testSchedulePassedDate:Sun Dec 25 16:11:32 CST 2011
Period:5秒
Task 預訂執行時間:Sun Dec 25 16:11:32 CST 2011, 
實際執行時間:Sun Dec 25 16:11:32 CST 2011
Task 預訂執行時間:Sun Dec 25 16:11:37 CST 2011, 
實際執行時間:Sun Dec 25 16:11:37 CST 2011
Task 預訂執行時間:Sun Dec 25 16:11:42 CST 2011, 
實際執行時間:Sun Dec 25 16:11:42 CST 2011
Task 預訂執行時間:Sun Dec 25 16:11:47 CST 2011, 
實際執行時間:Sun Dec 25 16:11:47 CST 2011
Task 預訂執行時間:Sun Dec 25 16:11:52 CST 2011, 
實際執行時間:Sun Dec 25 16:11:52 CST 2011
Task 預訂執行時間:Sun Dec 25 16:11:57 CST 2011, 
實際執行時間:Sun Dec 25 16:11:57 CST 2011
End testSchedulePassedDate:Sun Dec 25 16:12:02 CST 2011

In testScheduleAtFixedRatePassedDate:Sun Dec 25 16:12:02 CST 2011
Period:5秒
Task 預訂執行時間:Sun Dec 25 16:11:47 CST 2011, 
實際執行時間:Sun Dec 25 16:12:02 CST 2011
Task 預訂執行時間:Sun Dec 25 16:11:52 CST 2011, 
實際執行時間:Sun Dec 25 16:12:02 CST 2011
Task 預訂執行時間:Sun Dec 25 16:11:57 CST 2011, 
實際執行時間:Sun Dec 25 16:12:02 CST 2011
Task 預訂執行時間:Sun Dec 25 16:12:02 CST 2011, 
實際執行時間:Sun Dec 25 16:12:02 CST 2011
Task 預訂執行時間:Sun Dec 25 16:12:07 CST 2011, 
實際執行時間:Sun Dec 25 16:12:07 CST 2011
Task 預訂執行時間:Sun Dec 25 16:12:12 CST 2011, 
實際執行時間:Sun Dec 25 16:12:12 CST 2011
Task 預訂執行時間:Sun Dec 25 16:12:17 CST 2011, 
實際執行時間:Sun Dec 25 16:12:17 CST 2011
Task 預訂執行時間:Sun Dec 25 16:12:22 CST 2011, 
實際執行時間:Sun Dec 25 16:12:22 CST 2011
Task 預訂執行時間:Sun Dec 25 16:12:27 CST 2011, 
實際執行時間:Sun Dec 25 16:12:27 CST 2011
Task 預訂執行時間:Sun Dec 25 16:12:32 CST 2011, 
實際執行時間:Sun Dec 25 16:12:32 CST 2011
End testScheduleAtFixedRatePassedDate:Sun Dec 25 16:12:32 CST 2011
由上面的結果我們可以發現當現在的時間已經超過指定的 date 時,
schedule 會直接從現在開始做,且沒有 catch up 的情況。
而 scheduleAtFixedRate 則會發生 catch up,即他會想要趕上預訂的執行時間,
所以已過期的任務仍會被執行,這也是為何藍色部份會在一開始被執行連續被執行。

從以上所有的測試結果來看,兩者主要的差異有兩點:
  1. 執行工作時間較預訂工作間隔長時:
    schedule 會直接 delay 後續的工作預訂的時間;
    scheduleAtFixedRate 後續工作的預訂時間仍按工作間隔計算,
    後續若有工作提早完成,會以 catch up 來追上預訂時間。
  2. 執行任務已過期時:
    若只執行單次不repeat,則兩者都不會執行;但若有 repeat,
    schedule 不會管前面過期的任務,直接由當下開始執行並計算後續的工作時間;
    scheduleAtFixedRate 則一樣以 catch up 機制,會先補足前面未完成的部份。
其他關於 Timer 的細節有興趣的人可以參考 Timer 的 source code:
http://kickjava.com/src/java/util/Timer.java.htm

若想知道更多有關 Java 時間相關的轉換、排程等應用,請見:
Java 時間日期處理範例大全:含時間單位格式轉換、期間計算、排程等

關鍵字:java, timer, schedule, scheduleAtFixedRate, 差異, 不同, 比較, 差別, example, 例子, 範例,
參考資料:


更多精選推薦文章