併發程式設計 —— Timer 原始碼分析

莫那·魯道發表於2019-03-02

前言

在平時的開發中,肯定需要使用定時任務,而 Java 1.3 版本提供了一個 java.util.Timer 定時任務類。今天一起來看看這個類。

1.API 介紹

Timer 相關的有 3 個類:

Timer :面向程式設計師的API 都在這個類中。
TaskQuue: 儲存任務。
TimerThread: 執行任務的執行緒。

這個類的構造方法有 4 個:

Timer()                               建立一個新計時器。
Timer(boolean isDaemon)               建立一個新計時器,可以指定其相關的執行緒作為守護程式執行。
Timer(String name)                    建立一個新計時器,其相關的執行緒具有指定的名稱。
Timer(String name, boolean isDaemon)  建立一個新計時器,其相關的執行緒具有指定的名稱,並且可以指定作為守護程式執行。
複製程式碼

程式設計師可以使用的 API 如下:

void cancel()                                                          終止此計時器,丟棄所有當前已安排的任務。
int	purge()                                                            從此計時器的任務佇列中移除所有已取消的任務。
void schedule(TimerTask task, Date time)                               安排在指定的時間執行指定的任務。
void schedule(TimerTask task, Date firstTime, long period)             安排指定的任務在指定的時間開始進行重複的固定延遲執行。
void schedule(TimerTask task, long delay)                              安排在指定延遲後執行指定的任務。
void schedule(TimerTask task, long delay, long period)                 安排指定的任務從指定的延遲後開始進行重複的固定延遲執行。
void scheduleAtFixedRate(TimerTask task, Date firstTime, long period)  安排指定的任務在指定的時間開始進行重複的固定速率執行。
void scheduleAtFixedRate(TimerTask task, long delay, long period)      安排指定的任務在指定的延遲後開始進行重複的固定速率執行。
複製程式碼

下面從幾個具有代表性的方法開始分析 Timer 的原始碼。

2. 從構造方法開始

Timer timer = new Timer();

public Timer() {
    this("Timer-" + serialNumber());
}

public Timer(String name) {
    thread.setName(name);
    thread.start();
}

/**
 * The timer thread.
 */
private final TimerThread thread = new TimerThread(queue);

private final TaskQueue queue = new TaskQueue();

private TimerTask[] queue = new TimerTask[128];
複製程式碼

從上面一連串的構造方法中,可以看出,Timer 內部使用了一個執行緒 TimerThread,執行緒的構造引數是一個佇列(陣列)。

然後直接啟動了這個執行緒,預設是非守護模式的。

而這個執行緒的 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 方法,當任務結束後,清除佇列。並不在接受新的任務。

那麼這個 mainLoop 方法的邏輯是什麼呢?猜想一下,肯定是執行佇列中的任務。

private void mainLoop() {
    while (true) {
        try {
            TimerTask task;
            boolean taskFired;
            synchronized(queue) {
                // 如果佇列是空 且 newTasksMayBeScheduled 是 true,阻塞等待
                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) {
            // 如果有中斷異常就忽略。
        }
    }
}

複製程式碼

一如既往,寫了很多註釋,簡單說說邏輯:

  1. 死迴圈並鎖住佇列,因為這個 Timer 物件可能會被多個執行緒使用。
  2. 從佇列中取出任務。如果任務是重複執行的,就重新設定任務的執行時間。
  3. 執行任務的 run 方法。

這裡有幾個注意的地方:

  1. 該方法忽略了執行緒中斷異常。當 wait 方法中斷異常的時候,是不起作用的。
  2. 該方法值只捕獲執行緒中斷異常,如果發生了其他異常,整個 Timer 就會停止。

So,一定不要在自己的任務裡丟擲異常,否則一定會影響整個定時任務。

3. schedule 方法

timer.schedule(new MyTask(), 1000, 2000);
複製程式碼

以上定義了一個任務,1 秒後執行,重複執行時間 2 秒。

schedule 程式碼如下:

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);
}
複製程式碼

在 dealy 時間的基礎上,加上了當前時間,將 period 變成負數。

看看 sched 方法實現:

private void sched(TimerTask task, long time, long period) {
    if (time < 0)
        throw new IllegalArgumentException("Illegal execution time.");

    // 防止數值溢位
    if (Math.abs(period) > (Long.MAX_VALUE >> 1))
        period >>= 1;

    synchronized(queue) {
        // 如果該變數是 false ,說明任務執行緒停止了,丟擲異常
        if (!thread.newTasksMayBeScheduled)
            throw new IllegalStateException("Timer already cancelled.");

        synchronized(task.lock) {
            // 如果任務狀態不是純潔的初始狀態,丟擲異常
            if (task.state != TimerTask.VIRGIN)
                throw new IllegalStateException(
                    "Task already scheduled or cancelled");

            // 這隻下次執行時間
            task.nextExecutionTime = time;
            task.period = period;
            task.state = TimerTask.SCHEDULED;
        }
        // 新增進佇列末尾
        queue.add(task);
        // 如果獲取到第一個任務就剛剛新增的任務,說明執行緒阻塞了,喚醒他。
        if (queue.getMin() == task)
            queue.notify();
    }
}
複製程式碼

總結一下該方法,將任務新增進佇列,如果排程執行緒結束了,就丟擲異常—— 不能再新增。如果新增成功之後,獲取到的第一個任務就是這個任務,說明排程執行緒阻塞了,那就喚醒他。

4.總結

從一個定時任務的角度講,Timer 非常的簡單,使用一個執行緒,使用一個佇列。在簡單的場合,Timer 確實能夠滿足需求,但 Timer 還是有很多的缺陷:

  1. 不能 catch 住非執行緒中斷異常,如果使用者任務異常,將會導致整個 Timer 停止。

  2. 預設情況下不是守護執行緒,也就是說,他會阻止應用程式停止。你可以使用 cancel 方法停止他。

  3. 如果 Timer 因為 stop 方法獲取使用者任務異常終止了,那麼將再也不能向佇列中新增任務了。否則丟擲異常。

  4. 如果某個任務的執行時間太長,那麼他將會 “獨佔” 計時器的任務執行現場。導致延遲後續任務的執行,並且會將任務 “堆” 在一起。

So, 大規模的生產環境中,不建議使用 Timer,而是使用 JUC 的 ScheduledThreadPoolExecutor。樓主將在後面的文章中分析 ScheduledThreadPoolExecutor 的實現,相比較 Timer 有什麼好處。

相關文章