前言
在作後臺任務的時候常常須要實現各類各類的定時的,週期性的任務。好比每隔一段時間更新一下緩存之類的。一般週期性的任務均可以使用以下方式實現:html
- class MyTimerThread extends Thread {
- @Override
- public void run() {
- while(true) {
- try {
- Thread.sleep(60*1000);
-
- //每隔1分鐘須要執行的任務
- doTask();
-
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- };
- }
class MyTimerThread extends Thread {
@Override
public void run() {
while(true) {
try {
Thread.sleep(60*1000);
//每隔1分鐘須要執行的任務
doTask();
} catch (Exception e) {
e.printStackTrace();
}
}
};
}
其實用這種方式我還沒遇到過什麼問題。網上有人說調用線程sleep()方法會致使線程休眠時仍是會佔用cpu資源不釋放(而wait()不會),這種說法應該是不正確的。如有人知道其中存在的問題,敬請告知!因爲這種實現通常都是一個線程對於一個定時任務,且沒有實如今指定時間啓動任務(也能夠實現,加個時間判斷就能夠了)。
Timer簡介
JDK提供的Timer是很經常使用的定時任務調度器。在說到timer的原理時,咱們先看看Timer裏面的一些常見方法:
- /**
- * 這個方法是調度一個task,通過delay(ms)後開始進行調度,僅僅調度一次
- */
- public void schedule(TimerTask task, long delay)
-
- /**
- * 在指定的時間點time上調度一次
- */
- public void schedule(TimerTask task, Date time)
- 在指定的時間點time上調度一次。
-
- /**
- * 週期性調度任務,在delay(ms)後開始調度。
- * 而且任務開始時間的間隔爲period(ms),即「固定間隔」執行
- */
- public void schedule(TimerTask task, long delay, long period)
-
- /**
- * 和上一個方法相似,惟一的區別就是傳入的第二個參數爲第一次調度的時間
- */
- public void schedule(TimerTask task, Date firstTime, long period)
-
-
- public void scheduleAtFixedRate(TimerTask task, long delay, long period)
-
- public void scheduleAtFixedRate(TimerTask task, Date firstTime,long period)
/**
* 這個方法是調度一個task,通過delay(ms)後開始進行調度,僅僅調度一次
*/
public void schedule(TimerTask task, long delay)
/**
* 在指定的時間點time上調度一次
*/
public void schedule(TimerTask task, Date time)
在指定的時間點time上調度一次。
/**
* 週期性調度任務,在delay(ms)後開始調度。
* 而且任務開始時間的間隔爲period(ms),即「固定間隔」執行
*/
public void schedule(TimerTask task, long delay, long period)
/**
* 和上一個方法相似,惟一的區別就是傳入的第二個參數爲第一次調度的時間
*/
public void schedule(TimerTask task, Date firstTime, long period)
public void scheduleAtFixedRate(TimerTask task, long delay, long period)
public void scheduleAtFixedRate(TimerTask task, Date firstTime,long period)
不過比較很差理解的是Timer中,存在schedule和scheduleAtFixedRate兩套不一樣調度算法的方法,
它們的共同點是若判斷理論執行時間小於實際執行時間時,都會立刻執行任務,區別在於計算下一次執行時間的方式不一樣:
schedule: 任務開始的時間 + period(時間片斷),強調「固定間隔」地執行任務
scheduleAtFixedRate: 參數設定開始的時間 + period(時間片斷),強調「固定頻率」地執行任務
能夠看出前者採用實際值,後者採用理論值。不過實際上若參數設定的開始時間比當前時間大的話,二者執行的效果是同樣的。舉個反例說明:
- public static void main(String[] args) {
-
- TimerTask task = new TimerTask() {
- @Override
- public void run() {
- System.out.println(」do task…….」);
- }
- };
-
- SimpleDateFormat sdf = new SimpleDateFormat(「yyyy-MM-dd HH:mm:ss」);
- Timer timer = new Timer();
- try {
-
- timer.schedule(task, sdf.parse(」2016-4-9 00:00:00」), 5000);
-
- //timer.scheduleAtFixedRate(task, sdf.parse(「2016-4-9 00:00:00」),5000);
-
- } catch (ParseException e) {
- e.printStackTrace();
- }
- }
public static void main(String[] args) {
TimerTask task = new TimerTask() {
@Override
public void run() {
System.out.println("do task.......");
}
};
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Timer timer = new Timer();
try {
timer.schedule(task, sdf.parse("2016-4-9 00:00:00"), 5000);
//timer.scheduleAtFixedRate(task, sdf.parse("2016-4-9 00:00:00"),5000);
} catch (ParseException e) {
e.printStackTrace();
}
}
以上是參數設定時間比當前時間小的狀況,我在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:0五、00:00:十、00:00:1五、00:00:20、00:00:25……如今知道固定頻率的意思了吧!說好了要執行多少次就是多少次。
Timer的缺陷
Timer被設計成支持多個定時任務,經過源碼發現它有一個任務隊列用來存放這些定時任務,而且啓動了一個線程來處理,以下部分源碼所示:
- public class Timer {
-
- // 任務隊列
- private final TaskQueue queue = new TaskQueue();
-
- // 處理線程
- private final TimerThread thread = new TimerThread(queue);
public class Timer {
// 任務隊列
private final TaskQueue queue = new TaskQueue();
// 處理線程
private final TimerThread thread = new TimerThread(queue);
經過這種單線程的方式實現,在存在多個定時任務的時候便會存在問題:
若任務B執行時間過長,將致使任務A延遲了啓動時間!
還存在另一個問題,應該是屬於設計的問題:
若任務線程在執行隊列中某個任務時,該任務拋出異常,將致使線程因跳出循環體而終止,即Timer中止了工做!
一樣是舉個栗子:
- public static void main(String[] args) {
-
- Timer timer = new Timer();
-
- timer.schedule(new TimerTask() {
- @Override
- public void run() {
- SimpleDateFormat sdf = new SimpleDateFormat(「HH:mm:ss」);
- System.out.println(sdf.format(new Date()) + 「 A: do task」);
- }
- }, 0, 5*1000);
-
- timer.schedule(new TimerTask() {
- @Override
- public void run() {
- SimpleDateFormat sdf = new SimpleDateFormat(「HH:mm:ss」);
- System.out.println(sdf.format(new Date()) + 「 B: sleep」);
- try {
- Thread.sleep(20*1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }, 10*1000, 5000);
-
- timer.schedule(new TimerTask() {
- @Override
- public void run() {
- SimpleDateFormat sdf = new SimpleDateFormat(「HH:mm:ss」);
- System.out.println(sdf.format(new Date()) + 「 C: throw Exception」);
- throw new RuntimeException(「test」);
- }
- }, 30*1000, 5000);
- }
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
System.out.println(sdf.format(new Date()) + " A: do task");
}
}, 0, 5*1000);
timer.schedule(new TimerTask() {
@Override
public void run() {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
System.out.println(sdf.format(new Date()) + " B: sleep");
try {
Thread.sleep(20*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, 10*1000, 5000);
timer.schedule(new TimerTask() {
@Override
public void run() {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
System.out.println(sdf.format(new Date()) + " C: throw Exception");
throw new RuntimeException("test");
}
}, 30*1000, 5000);
}
經過以上程序發現:一開始,任務A能正常每隔5秒運行一次。在任務B啓動後,因爲任務B運行時間須要20秒,致使任務A要等到任務B執行完才能執行。更可怕的是,任務C啓動後,拋了個異常,定時任務掛了!
不過這種單線程的實現也有優勢:線程安全!
ScheduledThreadPoolExecutor簡介
ScheduledThreadPoolExecutor能夠說是Timer的多線程實現版本,連JDK官方都推薦使用ScheduledThreadPoolExecutor替代Timer。它是接口ScheduledExecutorService的子類,主要方法說明以下:
- /**
- * 調度一個task,通過delay(時間單位由參數unit決定)後開始進行調度,僅僅調度一次
- */
- public ScheduledFuture<?> schedule(Runnable command,
- long delay, TimeUnit unit);
-
- /**
- * 同上,支持參數不同
- */
- public <V> ScheduledFuture<V> schedule(Callable<V> callable,
- long delay, TimeUnit unit);
-
- /**
- * 週期性調度任務,在delay後開始調度,適合執行時間比「間隔」短的任務
- * 而且任務開始時間的間隔爲period,即「固定間隔」執行。
- * 若是任務執行的時間比period長的話,會致使該任務延遲執行,不會同時執行!
- * 若是任務執行過程拋出異常,後續不會再執行該任務!
- */
- public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
- long initialDelay ,long period ,TimeUnit unit);
-
- /**
- * Timer所沒有的「特點」方法,稱爲「固定延遲(delay)」調度,適合執行時間比「間隔」長的任務
- * 在initialDelay後開始調度該任務
- * 隨後,在每一次執行終止和下一次執行開始之間都存在給定的延遲period
- * 即下一次任務開始的時間爲:上一次任務結束時間(而不是開始時間) + delay時間
- * 若是任務執行過程拋出異常,後續不會再執行該任務!
- */
- public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
- long initialDelay ,long delay ,TimeUnit unit);
/**
* 調度一個task,通過delay(時間單位由參數unit決定)後開始進行調度,僅僅調度一次
*/
public ScheduledFuture<?> schedule(Runnable command,
long delay, TimeUnit unit);
/**
* 同上,支持參數不同
*/
public <V> ScheduledFuture<V> schedule(Callable<V> callable,
long delay, TimeUnit unit);
/**
* 週期性調度任務,在delay後開始調度,適合執行時間比「間隔」短的任務
* 而且任務開始時間的間隔爲period,即「固定間隔」執行。
* 若是任務執行的時間比period長的話,會致使該任務延遲執行,不會同時執行!
* 若是任務執行過程拋出異常,後續不會再執行該任務!
*/
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay ,long period ,TimeUnit unit);
/**
* Timer所沒有的「特點」方法,稱爲「固定延遲(delay)」調度,適合執行時間比「間隔」長的任務
* 在initialDelay後開始調度該任務
* 隨後,在每一次執行終止和下一次執行開始之間都存在給定的延遲period
* 即下一次任務開始的時間爲:上一次任務結束時間(而不是開始時間) + delay時間
* 若是任務執行過程拋出異常,後續不會再執行該任務!
*/
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay ,long delay ,TimeUnit unit);
ScheduledThreadPoolExecutor繼承自ThreadPoolExecutor,因此本質上說ScheduledThreadPoolExecutor仍是一個線程池(可參考
《Java線程池ThreadPoolExecutor簡介》)。它也有coorPoolSize和workQueue,接受Runnable的子類做爲任務。
特殊的地方在於它實現了本身的工做隊列DelayedWorkQueue,該任務隊列的做用是按照必定順序對隊列中的任務進行排序。好比,按照距離下次執行時間的長短的升序方式排列,讓須要儘快執行的任務排在隊首,「不那麼着急」的任務排在隊列後方,從而方便線程獲取到「應該」被執行的任務。除此以外,ScheduledThreadPoolExecutor還在任務執行結束後,計算出下次執行的時間,從新放到工做隊列中,等待下次調用。
上面經過一個程序說明了Timer存在的問題!這裏我將Timer換成了用ScheduledThreadPoolExecutor來實現,注意TimerTask也是Runnable的子類。
- public static void main(String[] args) {
- int corePoolSize = 3;
- ScheduledExecutorService pool = Executors.newScheduledThreadPool(corePoolSize);
-
- pool.scheduleAtFixedRate(new TimerTask() {
- @Override
- public void run() {
- SimpleDateFormat sdf = new SimpleDateFormat(「HH:mm:ss」);
- System.out.println(sdf.format(new Date()) + 「 A: do task」);
- }
- }, 0 ,5, TimeUnit.SECONDS);
-
- pool.scheduleAtFixedRate(new TimerTask() {
- @Override
- public void run() {
- SimpleDateFormat sdf = new SimpleDateFormat(「HH:mm:ss」);
- System.out.println(sdf.format(new Date()) + 「 B: sleep」);
- try {
- Thread.sleep(20*1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }, 10, 5, TimeUnit.SECONDS);
-
- pool.scheduleAtFixedRate(new TimerTask() {
- @Override
- public void run() {
- SimpleDateFormat sdf = new SimpleDateFormat(「HH:mm:ss」);
- System.out.println(sdf.format(new Date()) + 「 C: throw Exception」);
- throw new RuntimeException(「test」);
- }
- }, 30, 5, TimeUnit.SECONDS);
- }
public static void main(String[] args) {
int corePoolSize = 3;
ScheduledExecutorService pool = Executors.newScheduledThreadPool(corePoolSize);
pool.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
System.out.println(sdf.format(new Date()) + " A: do task");
}
}, 0 ,5, TimeUnit.SECONDS);
pool.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
System.out.println(sdf.format(new Date()) + " B: sleep");
try {
Thread.sleep(20*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, 10, 5, TimeUnit.SECONDS);
pool.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
System.out.println(sdf.format(new Date()) + " C: throw Exception");
throw new RuntimeException("test");
}
}, 30, 5, TimeUnit.SECONDS);
}
因爲有3個任務須要調度,所以我將corePoolSize設置爲3。經過控制檯打印能夠看到此次任務A一直都在正常運行(任務時間間隔爲5秒),並不受任務B的影響。任務C拋出異常後,雖然自己中止了調度,但沒有影響到其餘任務的調度。能夠說ScheduledThreadPoolExecutor解決Timer存在的問題!
那要是將corePoolSize設置爲1,變成單線程跑呢?結果固然是和Timer同樣,任務B會致使任務A延遲執行,不過比較好的是任務C拋異常不會影響到其餘任務的調度。
能夠說ScheduledThreadPoolExecutor適用於大部分場景,甚至就算timer提供的Date參數類型的開始時間也能夠經過本身轉的方式來實現。任務調度框架Quatz也是在ScheduledThreadPoolExecutor基礎上實現的。
通常咱們都使用單線程版的ScheduledThreadPoolExecutor居多,推薦經過如下方式來構建(構建後其線程數就不可更改):
- ScheduledExecutorService pool = Executors.newSingleThreadScheduledExecutor();
ScheduledExecutorService pool = Executors.newSingleThreadScheduledExecutor();
總結
不少時候真的不可能記得住這些類庫的特性,一不當心就會踩坑!好比我上面反覆強調的要是任務執行過程拋出異常了會怎麼怎麼樣,其實人家的API註釋是有說明的。另外是不肯定的仍是用經過寫demo來實踐一下,看看是否是真的這樣!還有就是除了看資料,寫demo,還能夠了解底層實現,這樣瞭解得更透徹。好比在若只有一個任務須要調度的狀況下,其實就算用Timer也是能夠的。
如上文有不正確的地方,感謝指點出來!
參考