Timer是進行任務調度的工具類,用來執行任務。Task可以被執行一次,也可以按照一個固定的時間間隔重複執行。
每個定時器對象都維護一個single後臺線程,它用於順序執行該定時器對象的所有任務。因此提交給Timer對象的任務應當能夠快速執行完。如果一個任務耗時太長,有可能導致後續任務延遲執行,或者導致很多個後續任務積壓,並且快速的串行執行完成。
當對Timer對象的最後一個引用消失,並且所有未完成的任務都已經完成執行時,執行task的thread將優雅的終止(terminates gracefully)並且成爲gc的目標。但是,如果想等待所有任務執行完成,可能會等待很長時間。默認情況下,執行task的thread(the task execution thread)不作爲守護線程(daemon thread),因此它會阻止應用程序關閉。如果調用者想要快速terminate Timer的任務執行線程,應該調用Timer類的取消方法。
如果調用了Timer類的cancel方法,接下來任何對任務進行調度的attempt都會導致一個IllegalStateException。
Timer類是線程安全的,在沒有額外同步措施的情況下,多個線程可以共享一個Timer對象。
Timer類不保證任務被調度的實時性:它基於Object.wait(long)方法進行調度任務。
jdk5引入了concurrent包,其中有一個併發工具類就是ScheduledThreadPoolExecutor,該線程池提供了一種在給定速率下貨延遲 重複執行任務的能力。它是Timer/TimerTask的一個更好的替代品,因爲它提供了多個線程執行任務,並且接受不同的時間單位(time unit),並且不需要接收一個TimerTask子類(只需要實現Runnable的任務即可)。
將ScheduledThreadPoolExecutor配置成一個線程將等價於Timer類。
Timer類可以適用於大量併發任務的執行(數千個任務應該沒有問題)。在該類內部,它使用了一個binary heap代表他的task queue,因此調度一個task的時間複雜度是O(log n)。(這是從調度最小堆的時間複雜度來分析,實際上如果一個timer對象同時處理數千個任務,那麼延遲可能會非常大)
所有該類的構造函數都會啓動一個timer thread。
Timer類有三個缺陷:
- 如上文所說,每個timer對象僅啓動一個線程執行任務,如果遇到耗時較長的任務,有可能造成後續任務延遲,或大量任務積壓。
- Timer類對調度的支持是基於絕對時間而不是相對時間,這意味着任務對於系統時鐘的改變是敏感的。
- Timer的另一個問題在於,如果TimerTask拋出未檢查的異常,Timer線程並不捕獲異常,所以TimerTask拋出的未檢查的異常會終止timer線程。這種情況下,Timer也不會再重新恢復線程的執行了;它錯誤的認爲整個Timer都被取消了。此時,已經被安排但尚未執行的TimerTask永遠不會再執行了,新的任務也不能被調度了:
public void run() {
try {
mainLoop();//一旦task拋出InterruptedException之外的Exception,mainLoop()方法將退出,向上拋出異常,導致執行finally塊,結束thread。
} finally {
// Someone killed this Thread, behave as if Timer cancelled
synchronized(queue) {
newTasksMayBeScheduled = false;
queue.clear(); // Eliminate obsolete references
}
}
}
下面重點說一下任務隊列這個數據結構。
Timer類維護了一個最小堆的任務隊列,底層爲數組,根據提交的任務的executeTime來維護隊列中任務的順序,最近需要執行的任務在堆上方,即最小堆(最小堆保證每個節點均小與等於其左子節點和右子節點)。
對於Timer類,在任務隊列上的操作主要爲:add一個任務、take一個min任務並remove。相對於直接使用有序數組存儲隊列來說,維護一個最小堆,其add和remove的時間複雜度由O(n)下降爲O(logn)。
TaskQueue的成員變量如下:
class TaskQueue {
private TimerTask[] queue = new TimerTask[128];
/**
* The number of tasks in the priority queue. (The tasks are stored in
* queue[1] up to queue[size]).
*/
private int size = 0;
爲了維護最小堆時移位方便,隊列選擇從index爲1的位置開始存儲元素。
add方法如下:
/**
* Adds a new task to the priority queue.
*/
void add(TimerTask task) {
// Grow backing store if necessary
if (size + 1 == queue.length)
queue = Arrays.copyOf(queue, 2*queue.length);
queue[++size] = task;
fixUp(size);
}
如果堆滿了,則進行擴容(2倍),之後把任務放到堆底,然後調整堆結構。fixUp方法如下:
private void fixUp(int k) {
while (k > 1) {
int j = k >> 1;
if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime)
break;
TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp;
k = j;
}
}
調整最小堆結構的方法爲:比較新增節點與其父節點大小,如果新增節點比較小,則交換兩個節點位置,否則結束循環;之後遞歸的進行判斷,直到遞歸到堆頂爲止。
隊列提供的另一個方法爲removeMin,即取出最小堆堆頂元素並且remove掉:
/**
* Remove the head task from the priority queue.
*/
void removeMin() {
queue[1] = queue[size];
queue[size--] = null; // Drop extra reference to prevent memory leak
fixDown(1);
}
removeMin的做法爲,把堆底元素放到堆頂,然後調整堆結構:
private void fixDown(int k) {
int j;
while ((j = k << 1) <= size && j > 0) {
if (j < size &&
queue[j].nextExecutionTime > queue[j+1].nextExecutionTime)
j++; // j indexes smallest kid
if (queue[k].nextExecutionTime <= queue[j].nextExecutionTime)
break;
TimerTask tmp = queue[j]; queue[j] = queue[k]; queue[k] = tmp;
k = j;
}
}
首先將k左移位,得到它的左子節點,將j指向左子節點和右子節點較小的那個,比較k與j爲止上任務的大小,如果k比較小,說明此時堆已有序,結束循環,否則交換k與j位置上的任務,繼續遞歸j。
通過fixUp和fixDown方法,保證了每次對最小堆add和remove方法時保持最小堆的特性。
接下來看一下Timer類是怎樣調度任務進行執行的。
每次創建一個Timer對象,其構造方法中就會啓動一個線程,從TaskQueue中獲取任務執行:
public Timer() {
this("Timer-" + serialNumber());
}
public Timer(String name) {
thread.setName(name);
thread.start();
}
thread是一個TimerThread類,繼承了Thread類,其run方法如下:
public void run() {
try {
mainLoop();
} finally {
// Someone killed this Thread, behave as if Timer cancelled
synchronized(queue) {
newTasksMayBeScheduled = false;
queue.clear(); // Eliminate obsolete references
}
}
}
mainLoop()方法如下:
/**
* The main timer loop. (See class comment.)
*/
private void mainLoop() {
while (true) {
try {
TimerTask task;
boolean taskFired;
synchronized(queue) {
// Wait for queue to become non-empty
while (queue.isEmpty() && newTasksMayBeScheduled)
queue.wait();
if (queue.isEmpty())
break; // Queue is empty and will forever remain; die
// Queue nonempty; look at first evt and do the right thing
long currentTime, executionTime;
task = queue.getMin();
synchronized(task.lock) {
if (task.state == TimerTask.CANCELLED) {
queue.removeMin();
continue; // No action required, poll queue again
}
currentTime = System.currentTimeMillis();
executionTime = task.nextExecutionTime;
if (taskFired = (executionTime<=currentTime)) {
if (task.period == 0) { // Non-repeating, remove
queue.removeMin();
task.state = TimerTask.EXECUTED;
} else { // Repeating task, reschedule
queue.rescheduleMin(
task.period<0 ? currentTime - task.period
: executionTime + task.period);
}
}
}
if (!taskFired) // Task hasn't yet fired; wait
queue.wait(executionTime - currentTime);
}
if (taskFired) // Task fired; run it, holding no locks
task.run();
} catch(InterruptedException e) {
}
}
}
mainLoop()方法是一個死循環,用來不停的從TaskQueue中獲取任務進行執行。執行任務時,首先獲取queue的對象鎖,判斷queue是否爲空,如果是則進行wait釋放queue的對象鎖,進入queue對象鎖的阻塞隊列中進行休眠。否則取出隊列堆頂任務,判斷其執行時間是否小於等於當前時間,如果不是,則繼續wait(executionTime - currentTime)一段時間再醒來執行;否則判斷任務的執行週期period,如果是0,說明任務只執行一次,此時將其從隊列中移除,並且將狀態標記爲EXECUTED;否則該任務屬於定時執行,則更新其在隊列中的executionTime,然後重新調整堆結構。最後調用TimerTask的run()方法進行執行任務。注意,此處直接調用的是Thread子類的run()方法而非start()方法,這意味着不會啓動新的線程來執行任務。
接下來看一下Timer類提交任務的方法:
public void schedule(TimerTask task, long delay, long period) {
if (delay < 0)
throw new IllegalArgumentException("Negative delay.");
if (period <= 0)
throw new IllegalArgumentException("Non-positive period.");
sched(task, System.currentTimeMillis()+delay, -period);
}
public void scheduleAtFixedRate(TimerTask task, long delay, long period) {
if (delay < 0)
throw new IllegalArgumentException("Negative delay.");
if (period <= 0)
throw new IllegalArgumentException("Non-positive period.");
sched(task, System.currentTimeMillis()+delay, period);
}
注意到兩個方法大致相同,接收一個任務,一個任務延遲時間,一個任務開始執行後的執行固定間隔。不過在調用內部sched方法傳遞period時,schedule方法傳遞的是負數,而scheduleAtFixedRate傳遞的是正數。這是爲什麼呢?
翻回頭看一下上面mainLoop()方法,在處理堆頂period非0的任務時,此任務爲按照固定間隔持續執行,需要把該任務的executeTime進行調整,然後調整堆結構:
queue.rescheduleMin(task.period<0 ? currentTime - task.period : executionTime + task.period);
可以看到,如果period小於0,那麼下一次的執行時間爲當前時間+period;如果period大於0,那麼下一次的執行時間爲executionTime+period。可是這有什麼區別呢,難道currentTime和executionTime不相同嗎?
確實不太相同,executionTime是小於等於currentTime的(看mainLoop,因爲已經進了if判斷,所以executionTime<=currentTime一定成立),可能由於gc線程或其他線程影響,佔據了較多cpu時間片,或者之前遇到了一個耗時較長的任務,導致timer任務執行線程執行當前任務有延遲,使executionTime < currentTime。
假設某一時間點currentTime爲10,executionTime爲6(因爲一些原因導致currentTime與executionTime有較大差異),period爲3,那麼使用schedule提交的方法由於period爲負數,以後的executionTime應爲13 16 19 22… 使用scheduleAtFixedRate提交的方法,以後的executionTime則爲 9 12 15 18。可以看到,schedule提交的任務能保證重複任務執行間隔永遠爲period(理論上),而scheduleAtFixedRate提交的任務在某些出現延遲的情況下會連續執行多個任務。也就是說,schedule基於實際執行時間,而scheduleAtFixedRate基於理論執行時間。
這會導致什麼問題呢,這會導致:schedule提交的任務保證了重複任務之間的間隔性,卻有可能導致在一個時間段內任務被少執行了(由於延遲);scheduleAtFixedRate提交的任務能夠保證在一個時間段內任務執行的次數,卻不能保證任務之間的間隔頻率。
因此在使用Timer類時,需要仔細考慮業務場景,選擇合適的方法進行任務提交。