Java中定時任務的實現:Timer與ScheduledExecutorService

本文轉載自:https://blog.csdn.net/guozebo/article/details/51090612

前言

在做後臺任務的時候經常需要實現各種各種的定時的,週期性的任務。比如每隔一段時間更新一下緩存之類的。通常週期性的任務都可以使用如下方式實現:

[java] view plain copy
  1. class MyTimerThread extends Thread {  
  2.     @Override  
  3.     public void run() {  
  4.         while(true) {  
  5.             try {  
  6.                 Thread.sleep(60*1000);  
  7.                   
  8.                 //每隔1分鐘需要執行的任務  
  9.                 doTask();  
  10.                   
  11.             } catch (Exception e) {  
  12.                 e.printStackTrace();  
  13.             }  
  14.         }  
  15.     };  
  16. }  
其實用這種方式我還沒遇到過什麼問題。網上有人說調用線程sleep()方法會導致線程休眠時還是會佔用cpu資源不釋放(而wait()不會),這種說法應該是不正確的。若有人知道其中存在的問題,敬請告知!由於這種實現一般都是一個線程對於一個定時任務,且沒有實現在指定時間啓動任務(也可以實現,加個時間判斷就可以了)。


Timer簡介

JDK提供的Timer是很常用的定時任務調度器。在說到timer的原理時,我們先看看Timer裏面的一些常見方法:
[java] view plain copy
  1. /** 
  2.  * 這個方法是調度一個task,經過delay(ms)後開始進行調度,僅僅調度一次 
  3.  */  
  4. public void schedule(TimerTask task, long delay)  
  5.   
  6. /** 
  7.  * 在指定的時間點time上調度一次 
  8.  */  
  9. public void schedule(TimerTask task, Date time)  
  10. 在指定的時間點time上調度一次。  
  11.   
  12. /** 
  13.  * 週期性調度任務,在delay(ms)後開始調度。 
  14.  * 並且任務開始時間的間隔爲period(ms),即“固定間隔”執行 
  15.  */  
  16. public void schedule(TimerTask task, long delay, long period)  
  17.   
  18. /** 
  19.  * 和上一個方法類似,唯一的區別就是傳入的第二個參數爲第一次調度的時間 
  20.  */  
  21. public void schedule(TimerTask task, Date firstTime, long period)  
  22.   
  23.   
  24. public void scheduleAtFixedRate(TimerTask task, long delay, long period)  
  25.   
  26. public void scheduleAtFixedRate(TimerTask task, Date firstTime,long period)  
不過比較不好理解的是Timer中,存在schedule和scheduleAtFixedRate兩套不同調度算法的方法,它們的共同點是若判斷理論執行時間小於實際執行時間時,都會馬上執行任務,區別在於計算下一次執行時間的方式不同:
schedule: 任務開始的時間 + period(時間片段),強調“固定間隔”地執行任務
scheduleAtFixedRate: 參數設定開始的時間 + period(時間片段),強調“固定頻率”地執行任務
可以看出前者採用實際值,後者採用理論值。不過實際上若參數設定的開始時間比當前時間大的話,兩者執行的效果是一樣的。舉個反例說明:
[java] view plain copy
  1. public static void main(String[] args) {  
  2.       
  3.     TimerTask task = new TimerTask() {  
  4.         @Override  
  5.         public void run() {  
  6.             System.out.println("do task.......");  
  7.         }  
  8.     };  
  9.       
  10.     SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  
  11.     Timer timer = new Timer();  
  12.     try {  
  13.           
  14.         timer.schedule(task, sdf.parse("2016-4-9 00:00:00"), 5000);  
  15.           
  16.         //timer.scheduleAtFixedRate(task, sdf.parse("2016-4-9 00:00:00"),5000);  
  17.   
  18.     } catch (ParseException e) {  
  19.         e.printStackTrace();  
  20.     }  
  21. }  
以上是參數設定時間比當前時間小的情況,我在2016-4-9 00:00:20時才啓動上面的程序:
對於schedule,打印了1條"do task"。因爲理論執行時間(00:00:00)小於實際執行時間(00:00:20)。然後等,因爲下一次執行的時間爲00:00:25。
對於scheduleAtFixedRate,打印了4條"do task"。因爲它的理論執行時間分別是00:00:05、00:00:10、00:00:15、00:00:20、00:00:25……現在知道固定頻率的意思了吧!說好了要執行多少次就是多少次。

Timer的缺陷

Timer被設計成支持多個定時任務,通過源碼發現它有一個任務隊列用來存放這些定時任務,並且啓動了一個線程來處理,如下部分源碼所示:
[java] view plain copy
  1. public class Timer {  
  2.   
  3.     // 任務隊列  
  4.     private final TaskQueue queue = new TaskQueue();  
  5.   
  6.     // 處理線程  
  7.     private final TimerThread thread = new TimerThread(queue);  
通過這種單線程的方式實現,在存在多個定時任務的時候便會存在問題:若任務B執行時間過長,將導致任務A延遲了啓動時間!
還存在另外一個問題,應該是屬於設計的問題:若任務線程在執行隊列中某個任務時,該任務拋出異常,將導致線程因跳出循環體而終止,即Timer停止了工作!
同樣是舉個栗子:
[java] view plain copy
  1. public static void main(String[] args) {  
  2.       
  3.     Timer timer = new Timer();  
  4.       
  5.     timer.schedule(new TimerTask() {  
  6.         @Override  
  7.         public void run() {  
  8.             SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");  
  9.             System.out.println(sdf.format(new Date()) + " A: do task");  
  10.         }  
  11.     }, 05*1000);  
  12.       
  13.     timer.schedule(new TimerTask() {  
  14.         @Override  
  15.         public void run() {  
  16.             SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");  
  17.             System.out.println(sdf.format(new Date()) + " B: sleep");  
  18.             try {  
  19.                 Thread.sleep(20*1000);  
  20.             } catch (InterruptedException e) {  
  21.                 e.printStackTrace();  
  22.             }  
  23.         }  
  24.     }, 10*10005000);  
  25.       
  26.     timer.schedule(new TimerTask() {  
  27.         @Override  
  28.         public void run() {  
  29.             SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");  
  30.             System.out.println(sdf.format(new Date()) + " C: throw Exception");  
  31.             throw new RuntimeException("test");  
  32.         }  
  33.     }, 30*10005000);  
  34. }  
通過以上程序發現:一開始,任務A能正常每隔5秒運行一次。在任務B啓動後,由於任務B運行時間需要20秒,導致任務A要等到任務B執行完才能執行。更可怕的是,任務C啓動後,拋了個異常,定時任務掛了!
不過這種單線程的實現也有優點:線程安全!

ScheduledThreadPoolExecutor簡介

ScheduledThreadPoolExecutor可以說是Timer的多線程實現版本,連JDK官方都推薦使用ScheduledThreadPoolExecutor替代Timer。它是接口ScheduledExecutorService的子類,主要方法說明如下:
[java] view plain copy
  1. /** 
  2.  * 調度一個task,經過delay(時間單位由參數unit決定)後開始進行調度,僅僅調度一次 
  3.  */  
  4. public ScheduledFuture<?> schedule(Runnable command,  
  5.                                        long delay, TimeUnit unit);  
  6.   
  7. /** 
  8.  * 同上,支持參數不一樣 
  9.  */  
  10. public <V> ScheduledFuture<V> schedule(Callable<V> callable,  
  11.                                            long delay, TimeUnit unit);  
  12.   
  13. /** 
  14.  * 週期性調度任務,在delay後開始調度,適合執行時間比“間隔”短的任務 
  15.  * 並且任務開始時間的間隔爲period,即“固定間隔”執行。 
  16.  * 如果任務執行的時間比period長的話,會導致該任務延遲執行,不會同時執行! 
  17.  * 如果任務執行過程拋出異常,後續不會再執行該任務! 
  18.  */  
  19. public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,  
  20.                         long initialDelay ,long period ,TimeUnit unit);  
  21.   
  22. /** 
  23.  * Timer所沒有的“特色”方法,稱爲“固定延遲(delay)”調度,適合執行時間比“間隔”長的任務 
  24.  * 在initialDelay後開始調度該任務 
  25.  * 隨後,在每一次執行終止和下一次執行開始之間都存在給定的延遲period 
  26.  * 即下一次任務開始的時間爲:上一次任務結束時間(而不是開始時間) + delay時間 
  27.  * 如果任務執行過程拋出異常,後續不會再執行該任務! 
  28.  */  
  29. public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,  
  30.                         long initialDelay ,long delay ,TimeUnit unit);  

ScheduledThreadPoolExecutor繼承自ThreadPoolExecutor,所以本質上說ScheduledThreadPoolExecutor還是一個線程池(可參考《Java線程池ThreadPoolExecutor簡介》)。它也有coorPoolSize和workQueue,接受Runnable的子類作爲任務。
特殊的地方在於它實現了自己的工作隊列DelayedWorkQueue,該任務隊列的作用是按照一定順序對隊列中的任務進行排序。比如,按照距離下次執行時間的長短的升序方式排列,讓需要儘快執行的任務排在隊首,“不那麼着急”的任務排在隊列後方,從而方便線程獲取到“應該”被執行的任務。除此之外,ScheduledThreadPoolExecutor還在任務執行結束後,計算出下次執行的時間,重新放到工作隊列中,等待下次調用。

上面通過一個程序說明了Timer存在的問題!這裏我將Timer換成了用ScheduledThreadPoolExecutor來實現,注意TimerTask也是Runnable的子類。
[java] view plain copy
  1. public static void main(String[] args) {  
  2.     int corePoolSize = 3;  
  3.     ScheduledExecutorService pool = Executors.newScheduledThreadPool(corePoolSize);    
  4.          
  5.        pool.scheduleAtFixedRate(new TimerTask() {  
  6.         @Override  
  7.         public void run() {  
  8.             SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");  
  9.             System.out.println(sdf.format(new Date()) + " A: do task");  
  10.         }  
  11.     }, 0 ,5, TimeUnit.SECONDS);    
  12.       
  13.        pool.scheduleAtFixedRate(new TimerTask() {  
  14.         @Override  
  15.         public void run() {  
  16.             SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");  
  17.             System.out.println(sdf.format(new Date()) + " B: sleep");  
  18.             try {  
  19.                 Thread.sleep(20*1000);  
  20.             } catch (InterruptedException e) {  
  21.                 e.printStackTrace();  
  22.             }  
  23.         }  
  24.     }, 105, TimeUnit.SECONDS);  
  25.       
  26.        pool.scheduleAtFixedRate(new TimerTask() {  
  27.         @Override  
  28.         public void run() {  
  29.             SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");  
  30.             System.out.println(sdf.format(new Date()) + " C: throw Exception");  
  31.             throw new RuntimeException("test");  
  32.         }  
  33.     }, 305, TimeUnit.SECONDS);  
  34. }  
由於有3個任務需要調度,因此我將corePoolSize設置爲3。通過控制檯打印可以看到這次任務A一直都在正常運行(任務時間間隔爲5秒),並不受任務B的影響。任務C拋出異常後,雖然本身停止了調度,但沒有影響到其他任務的調度。可以說ScheduledThreadPoolExecutor解決Timer存在的問題!
那要是將corePoolSize設置爲1,變成單線程跑呢?結果當然是和Timer一樣,任務B會導致任務A延遲執行,不過比較好的是任務C拋異常不會影響到其他任務的調度。

可以說ScheduledThreadPoolExecutor適用於大部分場景,甚至就算timer提供的Date參數類型的開始時間也可以通過自己轉的方式來實現。任務調度框架Quatz也是在ScheduledThreadPoolExecutor基礎上實現的。

一般我們都使用單線程版的ScheduledThreadPoolExecutor居多,推薦通過以下方式來構建(構建後其線程數就不可更改):
[java] view plain copy
  1. ScheduledExecutorService pool = Executors.newSingleThreadScheduledExecutor();  


總結

很多時候真的不可能記得住這些類庫的特性,一不小心就會踩坑!比如我上面反覆強調的要是任務執行過程拋出異常了會怎麼怎麼樣,其實人家的API註釋是有說明的。另外是不確定的還是用通過寫demo來實踐一下,看看是不是真的這樣!還有就是除了看資料,寫demo,還可以瞭解底層實現,這樣瞭解得更透徹。比如在若只有一個任務需要調度的情況下,其實就算用Timer也是可以的。
如上文有不正確的地方,感謝指點出來!



參考


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章