Timer類jdk文檔翻譯及源碼分析

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類有三個缺陷:

  1. 如上文所說,每個timer對象僅啓動一個線程執行任務,如果遇到耗時較長的任務,有可能造成後續任務延遲,或大量任務積壓。
  2. Timer類對調度的支持是基於絕對時間而不是相對時間,這意味着任務對於系統時鐘的改變是敏感的。
  3. 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類時,需要仔細考慮業務場景,選擇合適的方法進行任務提交。

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