Java 定時器 Timer 源碼分析和使用建議

Timer 定時器相信都不會陌生,之因此拿它來作源碼分析,是發現整個控制流程能夠體現不少有意思的東西。html

在業務開發中常常會遇到執行一些簡單定時任務的需求,一般爲了不作一些看起來複雜的控制邏輯,通常考慮使用 Timer 來實現定時任務的執行,下面先給出一個最簡單用法的例子:java

Timer timer = new Timer();
TimerTask timerTask = new TimerTask() {
    @Override
    public void run() {
        // scheduledExecutionTime() 返回此任務最近開始執行的時間
        Date date = new Date(this.scheduledExecutionTime());
        System.out.println("timeTask run " + date);
    }
};

// 從如今開始每間隔 1000 ms 計劃執行一個任務
timer.schedule(timerTask, 0, 1000);
複製代碼

Timer 概述

Timer 能夠按計劃執行重複的任務或者定時執行指定任務,這是由於 Timer 內部利用了一個後臺線程 TimerThread 有計劃地執行指定任務。算法

  • **Timer:**是一個實用工具類,該類用來調度一個線程(schedule a thread),使它能夠在未來某一時刻執行。 Java 的 Timer 類能夠調度一個任務運行一次或按期循環運行。 Timer tasks should complete quickly. 即定時器中的操做要儘量花費短的時間。segmentfault

  • **TimerTask:**一個抽象類,它實現了 Runnable 接口。咱們須要擴展該類以便建立本身的 TimerTask ,這個 TimerTask 能夠被 Timer 調度。數組

一個 Timer 對象對應的是單個後臺線程,其內部維護了一個 TaskQueue,用於順序執行計時器任務 TimeTask 。安全

Timer

Timer 中優先隊列的實現

TaskQueue 隊列,內部用一個 TimerTask[] 數組實現優先隊列(二叉堆),默認最大任務數是 128 ,當添加定時任務超過當前最大容量時會這個數組會拓展到原來 2 倍。數據結構

TaskQueue

優先隊列主要目的是爲了找出、返回並刪除優先隊列中最小的元素,這裏優先隊列是經過數組實現了平衡二叉堆,TimeQueue 實現的二叉堆用數組表示時,具備最小 nextExecutionTime 的 TimerTask 在隊列中爲 queue[1] ,因此堆中根節點在數組中的位置是 queue[1] ,那麼第 n 個位置 queue[n] 的子節點分別在 queue[2n] 和 queue[2n+1] 。關於優先隊列的數據結構實現,這裏推薦一篇文章:數據結構與算法學習筆記 - 優先隊列、二叉堆、左式堆多線程

按照 TaskQueue 的描述:This class represents a timer task queue: a priority queue of TimerTasks, ordered on nextExecutionTime.這是一個優先隊列,隊列的優先級按照 nextExecutionTime 進行調度。 也就說 TaskQueue 按照 TimerTask 的 nextExecutionTime 屬性界定優先級,優先級高的任務先出隊列,也就先執行任務調度。併發

隊列操做

如上圖所示,列舉了優先隊列中部分操做的實現,優先隊列插入和刪除元素的複雜度都是O(logn),因此add, removeMin 和 rescheduleMin方法的性能都是不錯的。從上圖能夠知道,獲取下一個計劃執行任務時,取隊列的頭出列便可,爲了減小額外性能消耗,移除隊列頭部元素的操做是先把隊尾元素賦值到隊首後,再把隊尾置空,隊列數量完成減一後進行優先權值操做。再下面看看保證優先隊列最核心的兩個方法fixUpfixDownide

兩個方法的核心思路都是經過向上或向下調整二叉堆中元素所在位置,保持堆的有序性: fixUp 是將元素值小於父節點的子節點與父節點交換位置,保持堆有序。交換位置後,原來的子節點可能仍然比更上層的父節點小, 因此整個過程須要循環進行。這樣一來,原來的子節點有可能升級爲層級更高的父節點,相似於一個輕的物體從湖底往上浮直到達到其重力與浮力相平衡的過程。 fixDown 將元素值大於子節點的父節點與子節點交換位置,交換位置後, 原來的父節點仍然有可能比其下面的子節點大, 因此還須要繼續進行類相同的操做,以便保持堆的有序性。因此整個過程循環進行。 這相似於一個重的物體從湖面下沉到距離湖底的某個位置,直到達到其重力與浮力相平衡爲止。 總的來講,就是調整大的元素下沉,小的元素上浮,反覆調整後堆頂一直保持是堆中最小的元素,父節點元素要一直小於或等於子節點。

TimerTask 的調度

前面說完 Timer 源碼中優先隊列的實現,下面咱們來看看其若是操做優先隊列,實現 TimerTask 的計劃調度的:

Timer 提供了四個構造方法,每一個構造方法都啓動了一個後臺線程(默認不是守護線程,除非主動指定)。因此對於每個 Timer 對象而言,其內部都是對應着單個後臺線程,這個線程用於順序執行優先隊列中全部的計時器任務。

Timer 構造器

當初始化完成 Timer 後,咱們就能夠往 Timer 中添加定時任務,而後定時任務就會按照咱們設定的時間交由 Timer 取調度執行。Timer 提供了 schedule 方法,該方法依靠屢次重載的方式來適應不一樣的狀況,具體以下:

  • **schedule(TimerTask task, Date time):**安排在指定的時間執行指定的任務。

  • **schedule(TimerTask task, long delay) :**安排在指定延遲後執行指定的任務。

  • **schedule(TimerTask task, Date firstTime, long period) :**安排指定的任務在指定的時間開始進行重複的固定延遲執行。

  • **schedule(TimerTask task, long delay, long period) :**安排指定的任務從指定的延遲後開始進行重複的固定延遲執行。

  • scheduleAtFixedRate :,scheduleAtFixedRate 方法與 schedule 相同,只不過他們的側重點不一樣,區別後面分析。

  • **scheduleAtFixedRate(TimerTask task, Date firstTime, long period):**安排指定的任務在指定的時間開始進行重複的固定速率執行。

  • **scheduleAtFixedRate(TimerTask task, long delay, long period):**安排指定的任務在指定的延遲後開始進行重複的固定速率執行。

首先來看 schedule(TimerTask task, Date time)schedule(TimerTask task, long delay) ,第一個參數傳入是定時任務的實例,區別在於方法的第二個參數,date 是在指定的時間點,delay 是當前時間延後多少毫秒。這就引出了 Timer 具備的兩個特性:定時(在指定時間點執行任務)和延遲(延遲多少秒後執行任務)。 值得你們注意的是:這裏所說時間都是跟系統時間相關的絕對時間,而不是相對時間,基於這點,Timer 對任務的調度計劃和系統時間息息相關,因此它對系統時間的改變很是敏感。

下面在來看看 schedule(TimerTask task, Date time)schedule(TimerTask task, Date firstTime, long period) 的區別。對比方法中新增的 period 參數,period 做用區別在於 Timer 的另外一個特性:週期性地執行任務(一次任務結束後,能夠每隔個 period 豪秒後再執行任務,如此反覆)。

從上面 schedule 的方法重載來看,最終都是調用了 sched(TimerTask task, long time, long period) 方法,只是傳入的參數不一樣,下面就再來看就看關於 schedule 和 scheduleAtFixedRate 的區別:

從調用方法來看,他們的區別僅僅是傳入 sched 方法 period 參數正負數的差異,因此具體的就要看 sched 方法的實現。

能夠看到 sched 方法主要是設置 TimerTask 屬性和狀態,好比 nextExecutionTime 等,而後將任務添加到隊列中。能看出來,設置定時任務 task 屬性時是加了鎖的,並且在添加任務到隊列時,這裏使用 Timer 內 TaskQueue 實例做爲對象鎖,而且使用 wait 和 notify 方法來通知任務調度。Timer 類能夠保證多個線程能夠共享單個 Timer 對象而無需進行外部同步,因此 Timer 類是線程安全的。

這裏注意區分開: 前面一個 Timer 對象中用於處理任務調度的後臺線程TimerThread 實例和 schedule 方法傳入後被加入到 TaskQueue 的 TimerTask 任務的實例,二者是不同的。

要想知道爲 TimerTask 設置屬性和狀態的做用,那就得進一步看看 TimerTask 類的具體實現了。

TimerTask 類是一個抽象類,能夠由 Timer 安排爲一次執行或重複執行的任務。它有一個抽象方法 run() 方法,用於子類實現 Runnale 接口。能夠在 run 方法中寫定時任務的具體業務邏輯。

TimerTask

能夠看到下圖中 TimerTask 類中的文檔描述,若是任務是按計劃執行,那麼 nextExecutionTime 屬性是指下次任務的執行時間,時間格式是按照 System.currentTimeMillis 返回的。對於須要重複進行的任務,每一個任務執行以前會更新這一屬性。

而 period 屬性是用來表示以毫秒爲時間單位的重複任務。period 爲正值時表示固定速率執行,負值表示固定延遲執行,值 0 表示一個非重複性的任務。

所謂固定速率執行和固定延遲執行,固定延遲指的是定時任務會由於前一個任務的延遲而致使其後面的定時任務延時,而固定速率執行則不會有這個問題,它是直接按照計劃的速率重複執行,不會考慮前面任務是否執行完。

這也是 scheduleAtFixedRate 與 schedule 方法的區別,二者側重點不一樣,schedule 方法側重保存間隔時間的穩定,而 scheduleAtFixedRate 方法更加側重於保持執行頻率的穩定。

另外 TimerTask 還有兩個非抽象方法:

  • **boolean cancel():**取消此計時器任務。
  • **long scheduledExecutionTime():**返回此任務最近實際執行的安排執行時間。

說完這些,下面就來看看 Timer 的後臺線程具體是如何調度隊列中的定時任務,能夠看到 TimerThread 是持有任務隊列進行操做的,也就具備了任務調度功能了。

下面就來看看後臺線程的 run 方法調用 mainLoop 具體作了什麼:

前面說到每一個 Timer 對象內部包含一個 TaskQueue 實例,在執行定時任務時,TimerThread 中將這個 taskqueue 對象做爲鎖,在任什麼時候刻只能有一個線程執行 TimerTask 。Timer 類爲了保證線程安全的,是不須要外部同步機制就能夠共享同一個 Timer 對象。

能夠看到 Timer 是不會捕獲異常的,若是 TimerTask 拋出的了未檢查異常則會致使 Timer 線程終止,同時 Timer 也不會從新恢復線程的執行,它會錯誤的認爲整個 Timer 線程都會取消。同時,已經被安排但還沒有執行的 TimerTask 也不會再執行了,新的任務也不能被調度。因此,若是 TimerTask 拋出未檢查的異常,Timer 將會產生沒法預料的行爲。

注意看計劃安排任務的核心代碼,包括任務計劃執行時間的設置,也有優先隊列保持二叉堆序性地操做。下面代碼很好地體現了 period 屬性做用,period 爲正值時表示固定速率執行,負值表示固定延遲執行,值 0 表示一個非重複性的任務。

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

前面提過 Timer 使用 schedule (TimerTask task, Date firstTime, long period) 方法執行的計時器任務可能會由於前一個任務執行時間較長而延時。每一次執行的 task 的計劃時間會隨着前一個 task 的實際時間而發生改變,也就是 scheduledExecutionTime(n+1) = realExecutionTime(n) + periodTime。也就是說若是第 n 個 task 因爲某種狀況致使此次的執行時間過程,最後致使 systemCurrentTime>= scheduledExecutionTime(n+1),這是第 n+1 個 task 並不會由於到時了而執行,他會等待第 n 個 task 執行完以後再執行,那麼這樣勢必會致使 n+2 個的執行時間 scheduledExecutionTime 發生改變。因此 schedule 方法更加註重保存間隔時間的穩定。

而對於 scheduleAtFixedRate(TimerTask task, Date firstTime, long period),在前面也提過 scheduleAtFixedRate 與 schedule 方法的側重點不一樣,schedule 方法側重保存間隔時間的穩定,而 scheduleAtFixedRate 方法更加側重於保持執行頻率的穩定。在 schedule 方法中會由於前一個任務的延遲而致使其後面的定時任務延時,而 scheduleAtFixedRate 方法則不會,若是第 n 個 task 執行時間過長致使 systemCurrentTime >= scheduledExecutionTime(n+1),則不會作任何等待他會當即執行第 n+1 個 task,因此 scheduleAtFixedRate 方法執行時間的計算方法不一樣於 schedule,而是 scheduledExecutionTime(n)=firstExecuteTime +n*periodTime,該計算方法永遠保持不變。因此 scheduleAtFixedRate 更加側重於保持執行頻率的穩定。

說完了 Timer 的源碼分析,相信大體上也能明白定時集整個流程是怎樣的。下面根據上面這些內容,說一些實際使用建議。

使用建議

最近使用阿里 Java 開發編碼規約插件,能夠看到提示是建議使用 ScheduledExecutorService 代替 Timer :

那爲何要使用 ScheduledExecutorService 代替 Timer :

  1. 前面咱們也有提到,Timer 是基於絕對時間的,對系統時間比較敏感,而 ScheduledThreadPoolExecutor 則是基於相對時間;

  2. Timer 是內部是單一線程,而 ScheduledThreadPoolExecutor 內部是個線程池,因此能夠支持多個任務併發執行。

  3. Timer 運行多個 TimeTask 時,只要其中之一沒有捕獲拋出的異常,其它任務便會自動終止運行,使用 ScheduledExecutorService 則沒有這個問題。

  4. 使用 ScheduledExecutorService 更容易明確任務實際執行策略,更方便自行控制。

  5. 默認 Timer 執行線程不是 daemon 線程, 任務執行完,主線程(或其餘啓動定時器的線程)結束時,task 線程並無結束。須要注意潛在內存泄漏問題

下面給出一個實際使用 ScheduledExecutorService 代替 Timer 的例子:

import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/** * ImprovedTimer 改進過的定時器 * 多線程並行處理定時任務時,Timer運行多個TimeTask時,只要其中之一沒有捕獲拋出的異常,其它任務便會自動終止運行, * 使用ScheduledExecutorService則沒有這個問題。 * * @author baishixian * @date 2017/10/16 * */

public class ImprovedTimer {


    /** * 線程池不容許使用Executors去建立,而是經過ThreadPoolExecutor的方式,這樣的處理方式讓寫的同窗更加明確線程池的運行規則,規避資源耗盡的風險。 說明:Executors各個方法的弊端: * 1)newFixedThreadPool和newSingleThreadExecutor: *   主要問題是堆積的請求處理隊列可能會耗費很是大的內存,甚至OOM。 * 2)newCachedThreadPool和newScheduledThreadPool: *   主要問題是線程數最大數是Integer.MAX_VALUE,可能會建立數量很是多的線程,甚至OOM。 * * 線程池能按時間計劃來執行任務,容許用戶設定計劃執行任務的時間,int類型的參數是設定 * 線程池中線程的最小數目。當任務較多時,線程池可能會自動建立更多的工做線程來執行任務 */
    private final ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1, new ImprovedTimer.DaemonThreadFactory());
    private ScheduledFuture<?> improvedTimerFuture = null;

    public ImprovedTimer() {
    }

    /** * 週期性重複執行定時任務 * @param command 執行 Runnable * @param initialDelay 單位 MILLISECONDS * @param period 單位 MILLISECONDS */
    public void schedule(Runnable command, long initialDelay, long period){
        // initialDelay 毫秒後開始執行任務,之後每隔 period 毫秒執行一次

        // schedule方法被用來延遲指定時間來執行某個指定任務。
        // 若是你須要週期性重複執行定時任務可使用scheduleAtFixedRate或者scheduleWithFixedDelay方法,它們不一樣的是前者以固定頻率執行,後者以相對固定頻率執行。
        // 無論任務執行耗時是否大於間隔時間,scheduleAtFixedRate和scheduleWithFixedDelay都不會致使同一個任務併發地被執行。
        // 惟一不一樣的是scheduleWithFixedDelay是當前一個任務結束的時刻,開始結算間隔時間,如0秒開始執行第一次任務,任務耗時5秒,任務間隔時間3秒,那麼第二次任務執行的時間是在第8秒開始。

        improvedTimerFuture = executorService.scheduleAtFixedRate(command, initialDelay, period, TimeUnit.MILLISECONDS);
    }

    /** * 週期性重複執行定時任務 * @param command 執行 Runnable * @param initialDelay 單位 MILLISECONDS */
    public void schedule(Runnable command, long initialDelay){
        // initialDelay 毫秒後開始執行任務

        improvedTimerFuture = executorService.schedule(command, initialDelay, TimeUnit.MILLISECONDS);
    }


    private void cancel() {
        if (improvedTimerFuture != null) {
            improvedTimerFuture.cancel(true);
            improvedTimerFuture = null;
        }
    }

    public void shutdown() {
        cancel();
        executorService.shutdown();
    }


    /** * 守護線程工廠類,用於生產後臺運行線程 */
    private static final class DaemonThreadFactory implements ThreadFactory {
        private AtomicInteger atoInteger = new AtomicInteger(0);

        @Override
        public Thread newThread(Runnable runnable) {
            Thread thread = new Thread(runnable);
            thread.setName("schedule-pool-Thread-" + atoInteger.getAndIncrement());
            thread.setDaemon(true);
            return thread;
        }
    }
}

複製代碼

參考: 詳解 Java 定時任務 Java多線程總結(3)— Timer 和 TimerTask深刻分析

OVER...

相關文章
相關標籤/搜索