深刻探究JDK中Timer的使用方式與源碼解析

導言

在項目開發過程當中,常常會遇到須要使用定時執行或延時執行任務的場景。好比咱們在活動結束後自動彙總生成效果數據、導出Excel表並將文件經過郵件推送到用戶手上,再好比微信運動天天都會在十點後向你發送個位數的步數(在?把攝像頭從我家拆掉!)。html

本文將會介紹java.util.Timer的使用,並從源碼層面對它進行解析。java

定時器Timer的使用

java.util.Timer是JDK提供的很是使用的工具類,用於計劃在特定時間後執行的任務,能夠只執行一次或按期重複執行。在JDK內部不少組件都是使用的java.util.Timer實現定時任務或延遲任務。api

Timer能夠建立多個對象的實例,每一個對象都有且只有一個後臺線程來執行任務。數組

Timer類是線程安全的,多個線程能夠共享一個計時器,而無需使用任何的同步。

構造方法

首先咱們能夠看下Timer類的構造方法的API文檔安全

 

  1. Timer(): 建立一個新的計時器。
  2. Timer(boolean isDaemon): 建立一個新的定時器,其關聯的工做線程能夠指定爲守護線程。
  3. Timer(String name): 建立一個新的定時器,其關聯的工做線程具備指定的名稱。
  4. Timer(String name, boolean isDaemon): 建立一個新的定時器,其相關線程具備指定的名稱,能夠指定爲守護線程。

Note: 守護線程是低優先級線程,在後臺執行次要任務,好比垃圾回收。當有非守護線程在運行時,Java應用不會退出。若是全部的非守護線程都退出了,那麼全部的守護線程也會隨之退出。微信

實例方法

接下來咱們看下Timer類的實例方法的API文檔數據結構

  1. cancel(): 終止此計時器,並丟棄全部當前執行的任務。
  2. purge(): 從該計時器的任務隊列中刪除全部取消的任務。
  3. schedule(TimerTask task, Date time): 在指定的時間執行指定的任務。
  4. schedule(TimerTask task, Date firstTime, long period): 從指定 的時間開始 ,對指定的任務按照固定的延遲時間重複執行 。
  5. schedule(TimerTask task, long delay): 在指定的延遲以後執行指定的任務。
  6. schedule(TimerTask task, long delay, long period): 在指定的延遲以後開始 ,對指定的任務按照固定的延遲時間重複執行 。
  7. scheduleAtFixedRate(TimerTask task, Date firstTime, long period): 從指定的時間開始 ,對指定的任務按照固定速率重複執行 。
  8. scheduleAtFixedRate(TimerTask task, long delay, long period): 在指定的延遲以後開始 ,對指定的任務按照固定速率重複執行。

schedulescheduleAtFixedRate都是重複執行任務,區別在於schedule是在任務成功執行後,再按照固定週期再從新執行任務,好比第一次任務從0s開始執行,執行5s,週期是10s,那麼下一次執行時間是15s而不是10s。而scheduleAtFixedRate是從任務開始執行時,按照固定的時間再從新執行任務,好比第一次任務從0s開始執行,執行5s,週期是10s,那麼下一次執行時間是10s而不是15s。oracle

使用方式

1. 執行時間晚於當前時間

接下來咱們將分別使用schedule(TimerTask task, Date time)schedule(TimerTask task, long delay)用來在10秒後執行任務,並展現是否將Timer的工做線程設置成守護線程對Timer執行的影響。ide

首先咱們建立類Task, 接下來咱們的全部操做都會在這個類中執行, 在類中使用schedule(TimerTask task, Date time),代碼以下函數

import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
import static java.text.MessageFormat.format;

public class Task {

    private static final long SECOND = 1000;

    public static void main(String[] args) {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println(format("程序結束時間爲: {0}", currentTimeMillis()));
        }));

        long startTimestamp = currentTimeMillis();
        System.out.println(format("程序執行時間爲: {0}", startTimestamp));
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                long exceptedTimestamp = startTimestamp + 10 * SECOND;
                long executingTimestamp = currentTimeMillis();
                long offset = executingTimestamp - exceptedTimestamp;
                System.out.println(format("任務運行在線程[{0}]上, 指望執行時間爲[{1}], 實際執行時間爲[{2}], 實際誤差[{3}]",
                        currentThread().getName(), exceptedTimestamp, executingTimestamp, offset));
            }
        }, new Date(startTimestamp + 10 * SECOND));
    }
}

在程序的最開始,咱們註冊程序結束時執行的函數,它用來打印程序的結束時間,咱們稍後將會用它來展現工做線程設置爲守護線程與非守護線程的差別。接下來是程序的主體部分,咱們記錄了程序的執行時間,定時任務執行時所在的線程、定時任務的指望執行時間與實際執行時間。

程序運行後的實際執行效果

程序執行時間爲: 1,614,575,921,461
任務運行在線程[Timer-0]上, 指望執行時間爲[1,614,575,931,461], 實際執行時間爲[1,614,575,931,464], 實際誤差[3]

程序在定時任務執行結束後並無退出,咱們註冊的生命週期函數也沒有執行,咱們將在稍後解釋這個現象。

接下來咱們在類中使用schedule(TimerTask task, long delay), 來達到相同的在10秒鐘以後執行的效果

import java.util.Timer;
import java.util.TimerTask;

import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
import static java.text.MessageFormat.format;

public class Task {

    private static final long SECOND = 1000;

    public static void main(String[] args) {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println(format("程序結束時間爲: {0}", currentTimeMillis()));
        }));

        Timer timer = new Timer();
        long startTimestamp = currentTimeMillis();
        System.out.println(format("程序執行時間爲: {0}", startTimestamp));
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                long exceptedTimestamp = startTimestamp + 10 * SECOND;
                long executingTimestamp = currentTimeMillis();
                long offset = executingTimestamp - exceptedTimestamp;
                System.out.println(format("任務運行在線程[{0}]上, 指望執行時間爲[{1}], 實際執行時間爲[{2}], 實際誤差[{3}]",
                        currentThread().getName(), exceptedTimestamp, executingTimestamp, offset));
            }
        }, 10 * SECOND);
    }
}

程序運行後的實際執行效果

程序執行時間爲: 1,614,576,593,325
任務運行在線程[Timer-0]上, 指望執行時間爲[1,614,576,603,325], 實際執行時間爲[1,614,576,603,343], 實際誤差[18]

回到咱們剛剛的問題上,爲何咱們的程序在執行完定時任務後沒有正常退出?咱們能夠從Java API中對Thread類的描述中找到相關的內容:

從這段描述中,咱們能夠看到,只有在兩種狀況下,Java虛擬機纔會退出執行

  1. 手動調用Runtime.exit()方法,而且安全管理器容許進行退出操做
  2. 全部的非守護線程都結束了,要麼是執行完run()方法,要麼是在run()方法中拋出向上傳播的異常

全部的Timer在建立後都會建立關聯的工做線程,這個關聯的工做線程默認是非守護線程的,因此很明顯咱們知足第二個條件,因此程序會繼續執行而不會退出。

那麼若是咱們將Timer的工做線程設置成守護線程會發生什麼呢?

import java.util.Timer;
import java.util.TimerTask;

import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
import static java.text.MessageFormat.format;

public class Task {

    private static final long SECOND = 1000;

    public static void main(String[] args) {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println(format("程序結束時間爲: {0}", currentTimeMillis()));
        }));

        Timer timer = new Timer(true);
        long startTimestamp = currentTimeMillis();
        System.out.println(format("程序執行時間爲: {0}", startTimestamp));
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                long exceptedTimestamp = startTimestamp + 10 * SECOND;
                long executingTimestamp = currentTimeMillis();
                long offset = executingTimestamp - exceptedTimestamp;
                System.out.println(format("任務運行在線程[{0}]上, 指望執行時間爲[{1}], 實際執行時間爲[{2}], 實際誤差[{3}]",
                        currentThread().getName(), exceptedTimestamp, executingTimestamp, offset));
            }
        }, 10 * SECOND);
    }
}

程序運行後的實際執行結果

程序執行時間爲: 1,614,578,037,976
程序結束時間爲: 1,614,578,037,996

能夠看到咱們的延遲任務尚未開始執行,程序就已經結束了,由於在咱們的主線程退出後,全部的非守護線程都結束了,因此Java虛擬機會正常退出,而不會等待Timer中全部的任務執行完成後再退出。

2. 執行時間早於當前時間

若是咱們是經過計算Date來指定執行時間的話,那麼不可避免會出現一個問題——計算後的時間是早於當前時間的,這很常見,尤爲是Java虛擬機會在不恰當的時候執行垃圾回收,並致使STW(Stop the world)。

接下來,咱們將調整以前調用schedule(TimerTask task, Date time)的代碼,讓它在過去的時間執行

import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
import static java.text.MessageFormat.format;

public class Task {

    private static final long SECOND = 1000;

    public static void main(String[] args) {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println(format("程序結束時間爲: {0}", currentTimeMillis()));
        }));

        Timer timer = new Timer();
        long startTimestamp = currentTimeMillis();
        System.out.println(format("程序執行時間爲: {0}", startTimestamp));
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                long exceptedTimestamp = startTimestamp - 10 * SECOND;
                long executingTimestamp = currentTimeMillis();
                long offset = executingTimestamp - exceptedTimestamp;
                System.out.println(format("任務運行在線程[{0}]上, 指望執行時間爲[{1}], 實際執行時間爲[{2}], 實際誤差[{3}]",
                        currentThread().getName(), exceptedTimestamp, executingTimestamp, offset));
            }
        }, new Date(startTimestamp - 10 * SECOND));
    }
}

程序運行後的執行結果

程序執行時間爲: 1,614,590,000,184
任務運行在線程[Timer-0]上, 指望執行時間爲[1,614,589,990,184], 實際執行時間爲[1,614,590,000,203], 實際誤差[10,019]

能夠看到,當咱們指定運行時間爲過去時間時,Timer的工做線程會立執行該任務。

可是若是咱們不是經過計算時間,而是指望延遲負數時間再執行,會發生什麼呢?咱們將調整以前調用schedule(TimerTask task, long delay)的代碼, 讓他以負數延遲時間執行

import java.util.Timer;
import java.util.TimerTask;

import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
import static java.text.MessageFormat.format;

public class Task {

    private static final long SECOND = 1000;

    public static void main(String[] args) {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println(format("程序結束時間爲: {0}", currentTimeMillis()));
        }));

        Timer timer = new Timer();
        long startTimestamp = currentTimeMillis();
        System.out.println(format("程序執行時間爲: {0}", startTimestamp));
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                long exceptedTimestamp = startTimestamp - 10 * SECOND;
                long executingTimestamp = currentTimeMillis();
                long offset = executingTimestamp - exceptedTimestamp;
                System.out.println(format("任務運行在線程[{0}]上, 指望執行時間爲[{1}], 實際執行時間爲[{2}], 實際誤差[{3}]",
                        currentThread().getName(), exceptedTimestamp, executingTimestamp, offset));
            }
        }, -10 * SECOND);
    }
}

程序運行後的執行結果

程序執行時間爲: 1,614,590,267,556
Exception in thread "main" java.lang.IllegalArgumentException: Negative delay.
	at java.base/java.util.Timer.schedule(Timer.java:193)
	at cn.mgdream.schedule.Task.main(Task.java:22)

若是咱們傳入負數的延遲時間,那麼Timer會拋出異常,告訴咱們不能傳入負數的延遲時間,這彷佛是合理的——咱們傳入過去的時間是由於這是咱們計算出來的,而不是咱們主觀傳入的。在咱們使用schedule(TimerTask task, long delay)須要注意這一點。

3. 向Timer中添加多個任務

接下來咱們將分別向Timer中添加兩個延遲任務,爲了更容易地控制兩個任務的調度順序和時間,咱們讓第一個任務延遲5秒,第二個任務延遲10秒,同時讓第一個任務阻塞10秒後再結束,經過這種方式來模擬出長任務。

import java.util.Timer;
import java.util.TimerTask;

import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
import static java.text.MessageFormat.format;

public class Task {

    private static final long SECOND = 1000;

    public static void main(String[] args) {
        Timer timer = new Timer();
        long startTimestamp = currentTimeMillis();
        System.out.println(format("程序執行時間爲: {0}", startTimestamp));
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                try {
                    long exceptedTimestamp = startTimestamp + 5 * SECOND;
                    long executingTimestamp = currentTimeMillis();
                    long offset = executingTimestamp - exceptedTimestamp;
                    System.out.println(format("任務[0]運行在線程[{0}]上, 指望執行時間爲[{1}], 實際執行時間爲[{2}], 實際誤差[{3}]",
                            currentThread().getName(), exceptedTimestamp, executingTimestamp, offset));
                    Thread.sleep(10 * SECOND);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, 5 * SECOND);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                long exceptedTimestamp = startTimestamp + 10 * SECOND;
                long executingTimestamp = currentTimeMillis();
                long offset = executingTimestamp - exceptedTimestamp;
                System.out.println(format("任務[1]運行在線程[{0}]上, 指望執行時間爲[{1}], 實際執行時間爲[{2}], 實際誤差[{3}]",
                        currentThread().getName(), exceptedTimestamp, executingTimestamp, offset));
            }
        }, 10 * SECOND);
    }
}

程序運行後的執行結果

程序執行時間爲: 1,614,597,388,284
任務[0]運行在線程[Timer-0]上, 指望執行時間爲[1,614,597,393,284], 實際執行時間爲[1,614,597,393,308], 實際誤差[24]
任務[1]運行在線程[Timer-0]上, 指望執行時間爲[1,614,597,398,284], 實際執行時間爲[1,614,597,403,312], 實際誤差[5,028]

能夠看到,兩個任務在同個線程順序執行,而第一個任務由於阻塞了10秒鐘,因此是在程序開始運行後的第15秒結束,而第二個任務指望在第10秒結束,可是由於第一個任務尚未結束,因此第二個任務在第15秒開始執行,與與其執行時間誤差5秒鐘。在使用Timer時儘量不要執行長任務或使用阻塞方法,不然會影響後續任務執行時間的準確性。

4. 週期性執行任務

接下來咱們將會分別使用schedulescheduleAtFixedRate實現週期性執行任務。爲了節省篇幅,咱們將只演示如何使用schedule(TimerTask task, long delay, long period)scheduleAtFixedRate(TimerTask task, long delay, long period)來實現週期性執行任務,並介紹它們的差別。而其餘的兩個方法schedule(TimerTask task, Date firstTime, long period)scheduleAtFixedRate(TimerTask task, Date firstTime, long period)具備相同的效果和差別,就再也不贅述。

首先咱們修改Task類,調用schedule(TimerTask task, long delay, long period)來實現第一次執行完延遲任務後,週期性地執行任務

import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicLong;

import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
import static java.text.MessageFormat.format;

public class Task {

    private static final long SECOND = 1000;

    public static void main(String[] args) {
        AtomicLong counter = new AtomicLong(0);
        Timer timer = new Timer();
        long startTimestamp = currentTimeMillis();
        System.out.println(format("程序執行時間爲: {0}", startTimestamp));
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                long count = counter.getAndIncrement();
                long exceptedTimestamp = startTimestamp + 10 * SECOND + count * SECOND;
                long executingTimestamp = currentTimeMillis();
                long offset = executingTimestamp - exceptedTimestamp;
                System.out.println(format("任務運行在線程[{0}]上, 指望執行時間爲[{1}], 實際執行時間爲[{2}], 實際誤差[{3}]",
                        currentThread().getName(), exceptedTimestamp, executingTimestamp, offset));
            }
        }, 10 * SECOND, SECOND);
    }
}

修改後的代碼和使用schedule(TimerTask task, long delay)時的代碼基本相同,咱們額外添加計數器來記錄任務的執行次數,方法調用添加了第三個參數period,表示任務每次執行時到下一次開始執行的時間間隔,咱們這裏設置成1秒鐘。

程序運行後的執行結果

程序執行時間爲: 1,614,609,111,434
任務運行在線程[Timer-0]上, 指望執行時間爲[1,614,609,121,434], 實際執行時間爲[1,614,609,121,456], 實際誤差[22]
任務運行在線程[Timer-0]上, 指望執行時間爲[1,614,609,122,434], 實際執行時間爲[1,614,609,122,456], 實際誤差[22]
任務運行在線程[Timer-0]上, 指望執行時間爲[1,614,609,123,434], 實際執行時間爲[1,614,609,123,457], 實際誤差[23]
任務運行在線程[Timer-0]上, 指望執行時間爲[1,614,609,124,434], 實際執行時間爲[1,614,609,124,462], 實際誤差[28]
任務運行在線程[Timer-0]上, 指望執行時間爲[1,614,609,125,434], 實際執行時間爲[1,614,609,125,467], 實際誤差[33]
任務運行在線程[Timer-0]上, 指望執行時間爲[1,614,609,126,434], 實際執行時間爲[1,614,609,126,470], 實際誤差[36]
任務運行在線程[Timer-0]上, 指望執行時間爲[1,614,609,127,434], 實際執行時間爲[1,614,609,127,473], 實際誤差[39]
任務運行在線程[Timer-0]上, 指望執行時間爲[1,614,609,128,434], 實際執行時間爲[1,614,609,128,473], 實際誤差[39]
任務運行在線程[Timer-0]上, 指望執行時間爲[1,614,609,129,434], 實際執行時間爲[1,614,609,129,474], 實際誤差[40]

能夠看到,每次任務執行都會有必定時間的誤差,而這個誤差隨着執行次數的增長而不斷積累。這個時間誤差取決於Timer中須要執行的任務的個數,隨着Timer中須要執行的任務的個數增長呈非遞減趨勢。由於這個程序如今只有一個任務在重複執行,所以每次執行的誤差不是很大,若是同時維護成百上千個任務,那麼這個時間誤差會變得很明顯。

接下來咱們修改Task類,調用scheduleAtFixedRate(TimerTask task, long delay, long period)來實現週期性執行任務

import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicLong;

import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
import static java.text.MessageFormat.format;

public class Task {

    private static final long SECOND = 1000;

    public static void main(String[] args) {
        AtomicLong counter = new AtomicLong(0);
        Timer timer = new Timer();
        long startTimestamp = currentTimeMillis();
        System.out.println(format("程序執行時間爲: {0}", startTimestamp));
        timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                long count = counter.getAndIncrement();
                long exceptedTimestamp = startTimestamp + 10 * SECOND + count * SECOND;
                long executingTimestamp = currentTimeMillis();
                long offset = executingTimestamp - exceptedTimestamp;
                System.out.println(format("任務運行在線程[{0}]上, 指望執行時間爲[{1}], 實際執行時間爲[{2}], 實際誤差[{3}]",
                        currentThread().getName(), exceptedTimestamp, executingTimestamp, offset));
            }
        }, 10 * SECOND, SECOND);
    }
}

方法scheduleAtFixedRate(TimerTask task, long delay, long period)schedule(TimerTask task, long delay)的效果基本相同,它們均可以達到週期性執行任務的效果,可是scheduleAtFixedRate方法會修正任務的下一次指望執行時間,按照每一次的指望執行時間加上period參數來計算出下一次指望執行時間,所以scheduleAtFixedRate是以固定速率重複執行的,而schedule則只保證兩次執行的時間間隔相同

程序運行後的執行結果

程序執行時間爲: 1,614,610,372,927
任務運行在線程[Timer-0]上, 指望執行時間爲[1,614,610,383,927], 實際執行時間爲[1,614,610,383,950], 實際誤差[23]
任務運行在線程[Timer-0]上, 指望執行時間爲[1,614,610,384,927], 實際執行時間爲[1,614,610,384,951], 實際誤差[24]
任務運行在線程[Timer-0]上, 指望執行時間爲[1,614,610,385,927], 實際執行時間爲[1,614,610,385,951], 實際誤差[24]
任務運行在線程[Timer-0]上, 指望執行時間爲[1,614,610,386,927], 實際執行時間爲[1,614,610,386,947], 實際誤差[20]
任務運行在線程[Timer-0]上, 指望執行時間爲[1,614,610,387,927], 實際執行時間爲[1,614,610,387,949], 實際誤差[22]
任務運行在線程[Timer-0]上, 指望執行時間爲[1,614,610,388,927], 實際執行時間爲[1,614,610,388,946], 實際誤差[19]
任務運行在線程[Timer-0]上, 指望執行時間爲[1,614,610,389,927], 實際執行時間爲[1,614,610,389,946], 實際誤差[19]
任務運行在線程[Timer-0]上, 指望執行時間爲[1,614,610,390,927], 實際執行時間爲[1,614,610,390,947], 實際誤差[20]
任務運行在線程[Timer-0]上, 指望執行時間爲[1,614,610,391,927], 實際執行時間爲[1,614,610,391,950], 實際誤差[23]
任務運行在線程[Timer-0]上, 指望執行時間爲[1,614,610,392,927], 實際執行時間爲[1,614,610,392,946], 實際誤差[19]

5. 中止任務

儘管咱們不多會主動中止任務,可是這裏仍是要介紹下任務中止的方式。

中止任務的方式分爲兩種:中止單個任務和中止整個Timer

首先咱們介紹如何中止單個任務,爲了中止單個任務,咱們須要調用TimerTaskcancal()方法,並調用Timerpurge()方法來移除全部已經被中止了的任務(回顧咱們以前提到的,過多中止的任務不清空會影響咱們的執行時間)

import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicLong;

import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
import static java.text.MessageFormat.format;

public class Task {

    private static final long SECOND = 1000;

    public static void main(String[] args) {
        AtomicLong counter = new AtomicLong(0);
        Timer timer = new Timer();
        long startTimestamp = currentTimeMillis();
        System.out.println(format("程序執行時間爲: {0}", startTimestamp));
        TimerTask[] timerTasks = new TimerTask[4096];
        for (int i = 0; i < timerTasks.length; i++) {
            final int serialNumber = i;
            timerTasks[i] = new TimerTask() {
                @Override
                public void run() {
                    long count = counter.getAndIncrement();
                    long exceptedTimestamp = startTimestamp + 10 * SECOND + count * SECOND;
                    long executingTimestamp = currentTimeMillis();
                    long offset = executingTimestamp - exceptedTimestamp;
                    System.out.println(format("任務[{0}]運行在線程[{1}]上, 指望執行時間爲[{2}], 實際執行時間爲[{3}], 實際誤差[{4}]",
                            serialNumber, currentThread().getName(), exceptedTimestamp, executingTimestamp, offset));
                }
            };
        }
        for (TimerTask timerTask : timerTasks) {
            timer.schedule(timerTask, 10 * SECOND, SECOND);
        }
        for (int i = 1; i < timerTasks.length; i++) {
            timerTasks[i].cancel();
        }
        timer.purge();
    }
}

首先咱們建立了4096個任務,並讓Timer來調度它們,接下來咱們把除了第0個任務外的其餘4095個任務中止掉,並從Timer中移除全部已經中止的任務。

程序運行後的執行結果

程序執行時間爲: 1,614,611,843,830
任務[0]運行在線程[Timer-0]上, 指望執行時間爲[1,614,611,853,830], 實際執行時間爲[1,614,611,853,869], 實際誤差[39]
任務[0]運行在線程[Timer-0]上, 指望執行時間爲[1,614,611,854,830], 實際執行時間爲[1,614,611,854,872], 實際誤差[42]
任務[0]運行在線程[Timer-0]上, 指望執行時間爲[1,614,611,855,830], 實際執行時間爲[1,614,611,855,875], 實際誤差[45]
任務[0]運行在線程[Timer-0]上, 指望執行時間爲[1,614,611,856,830], 實際執行時間爲[1,614,611,856,876], 實際誤差[46]
任務[0]運行在線程[Timer-0]上, 指望執行時間爲[1,614,611,857,830], 實際執行時間爲[1,614,611,857,882], 實際誤差[52]
任務[0]運行在線程[Timer-0]上, 指望執行時間爲[1,614,611,858,830], 實際執行時間爲[1,614,611,858,883], 實際誤差[53]
任務[0]運行在線程[Timer-0]上, 指望執行時間爲[1,614,611,859,830], 實際執行時間爲[1,614,611,859,887], 實際誤差[57]
任務[0]運行在線程[Timer-0]上, 指望執行時間爲[1,614,611,860,830], 實際執行時間爲[1,614,611,860,890], 實際誤差[60]
任務[0]運行在線程[Timer-0]上, 指望執行時間爲[1,614,611,861,830], 實際執行時間爲[1,614,611,861,891], 實際誤差[61]
任務[0]運行在線程[Timer-0]上, 指望執行時間爲[1,614,611,862,830], 實際執行時間爲[1,614,611,862,892], 實際誤差[62]

咱們能夠看到,只有第0個任務再繼續執行,而其餘4095個任務都沒有執行。

接下來咱們介紹如何使用Timercancel()來中止整個Timer的全部任務,其實很簡單,只須要執行timer.cancel()就能夠。

import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicLong;

import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
import static java.text.MessageFormat.format;

public class Task {

    private static final long SECOND = 1000;

    public static void main(String[] args) {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println(format("程序結束時間爲: {0}", currentTimeMillis()));
        }));
        
        AtomicLong counter = new AtomicLong(0);
        Timer timer = new Timer();
        long startTimestamp = currentTimeMillis();
        System.out.println(format("程序執行時間爲: {0}", startTimestamp));
        TimerTask[] timerTasks = new TimerTask[4096];
        for (int i = 0; i < timerTasks.length; i++) {
            final int serialNumber = i;
            timerTasks[i] = new TimerTask() {
                @Override
                public void run() {
                    long count = counter.getAndIncrement();
                    long exceptedTimestamp = startTimestamp + 10 * SECOND + count * SECOND;
                    long executingTimestamp = currentTimeMillis();
                    long offset = executingTimestamp - exceptedTimestamp;
                    System.out.println(format("任務[{0}]運行在線程[{1}]上, 指望執行時間爲[{2}], 實際執行時間爲[{3}], 實際誤差[{4}]",
                            serialNumber, currentThread().getName(), exceptedTimestamp, executingTimestamp, offset));
                }
            };
        }
        timer.cancel();
    }
}

在將全部的任務添加到Timer後,咱們執行Timer對象的cancel()方法,爲了更方便地表現出Timer的工做線程也終止了,咱們註冊了生命週期方法,來幫咱們在程序結束後打印結束時間。

程序運行後的執行結果

程序執行時間爲: 1,614,612,436,037
程序結束時間爲: 1,614,612,436,061

能夠看到,在執行Timer對象的cancel()方法後,Timer的工做線程也隨之結束,程序正常退出。

源碼解析

TimerTask


TimerTask類是一個抽象類,實現了Runnable接口

public abstract class TimerTask implements Runnable

TimerTask對象的成員

首先來看TimerTask類的成員部分

final Object lock = new Object();

int state = VIRGIN;

static final int VIRGIN      = 0;
static final int SCHEDULED   = 1;
static final int EXECUTED    = 2;
static final int CANCELLED   = 3;

long nextExecutionTime;

long period = 0;

對象lock是對外用來控制TimerTask對象修改的鎖對象,它控制了鎖的粒度——只會影響類屬性的變動,而不會影響整個類的方法調用。接下來是state屬性表示TimerTask對象的狀態。nextExecutionTime屬性表示TimerTask對象的下一次執行時間,當TimerTask對象被添加到任務隊列後,將會使用這個屬性來按照從小到大的順序排序。period屬性表示TimerTask對象的執行週期,period屬性的值有三種狀況

  1. 若是是0,那麼表示任務不會重複執行
  2. 若是是正數,那麼就表示任務按照相同的執行間隔來重複執行
  3. 若是是負數,那麼就表示任務按照相同的執行速率來重複執行

TimerTask對象的構造方法

Timer對象的構造方法很簡單,就是protected限定的默認構造方法,再也不贅述

protected TimerTask() {
}

TimerTask對象的成員方法

接下來咱們看下TimerTask對象的成員方法

public abstract void run();

public boolean cancel() {
    synchronized(lock) {
        boolean result = (state == SCHEDULED);
        state = CANCELLED;
        return result;
    }
}

public long scheduledExecutionTime() {
    synchronized(lock) {
        return (period < 0 ? nextExecutionTime + period
                           : nextExecutionTime - period);
    }
}

首先是run()方法實現自Runnable()接口,爲抽象方法,全部的任務都須要實現此方法。接下來是cancel()方法,這個方法會將任務的狀態標記爲CANCELLED,若是在結束前任務處於被調度狀態,那麼就返回true,不然返回false。至於scheduledExecutionTime()只是用來計算重複執行的下一次執行時間,在Timer中並無被使用過,再也不贅述。

TimerQueue


TimerQueueTimer維護任務調度順序的最小優先隊列,使用的是最小二叉堆實現,如上文所述,排序用的Key是TimerTasknextExecutionTime屬性。

在介紹TimerQueue以前,咱們先補充下數據結構的基礎知識

二叉堆(Binary heap)

二叉堆是一顆除了最底層的元素外,全部層都被填滿,最底層的元素從左向右填充的徹底二叉樹(complete binary tree)。徹底二叉樹能夠用數組表示,假設元素從1開始編號,下標爲i的元素,它的左孩子的下標爲2*i,它的右孩子的下標爲2*i+1

二叉堆的任意非葉節點知足堆序性:假設咱們定義的是最小優先隊列,那麼咱們使用的是小根堆,任意節點的元素值都小於它的左孩子和右孩子(若是有的話)的元素值。

二叉堆的定義知足遞歸定義法,即二叉堆的任意子樹都是二叉堆,單個節點自己就是二叉堆。

根據堆序性和遞歸定義法,二叉堆的根節點必定是整個二叉堆中元素值最小的節點

與堆結構有關的操做,除了add, getMinremoveMin以外,還有fixUpfixDownheapify三個關鍵操做,而addgetMinremoveMin也是經過這三個操做來完成的,下面來簡單介紹下這三個操做

  1. fixUp: 當咱們向二叉堆中添加元素時,咱們能夠簡單地將它添加到二叉樹的末尾,此時從這個節點到根的完整路徑上不知足堆序性。以後將它不斷向上浮,直到遇到比它小的元素,此時整個二叉樹的全部節點都知足堆序性。當咱們減小了二叉堆中元素的值的時候也能夠經過這個方法來維護二叉堆。
  2. fixDown: 當咱們從二叉堆中刪除元素時,咱們能夠簡單地將二叉樹末尾的元素移動到根,此時不必定知足堆序性,以後將它不斷下沉,直到遇到比它大的元素,此時整個二叉樹的全部節點都知足堆序性。當咱們增長了二叉堆中元素的值的時候也能夠經過這個方法來維護二叉堆。
  3. heapify: 當咱們拿到無序的數組的時候,也能夠假設咱們拿到了一棵不知足堆序性的二叉樹,此時咱們將全部的非葉節點向下沉,直到整個二叉樹的全部節點都知足堆序性,此時咱們獲得了完整的二叉堆。這個操做是原地操做,不須要額外的空間複雜度,而時間複雜度是O(N)。

關於二叉堆的詳細內容將會在後續的文章中展開詳解,這裏只作簡單的介紹,瞭解這些咱們就能夠開始看TimerQueue的源碼。

TimerQueue的完整代碼

咱們直接來看TaskQueue的完整代碼

class TaskQueue {

    private TimerTask[] queue = new TimerTask[128];

    private int size = 0;

    int size() {
        return size;
    }

    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);
    }

    TimerTask getMin() {
        return queue[1];
    }

    TimerTask get(int i) {
        return queue[i];
    }

    void removeMin() {
        queue[1] = queue[size];
        queue[size--] = null;  // Drop extra reference to prevent memory leak
        fixDown(1);
    }

    void quickRemove(int i) {
        assert i <= size;

        queue[i] = queue[size];
        queue[size--] = null;  // Drop extra ref to prevent memory leak
    }

    void rescheduleMin(long newTime) {
        queue[1].nextExecutionTime = newTime;
        fixDown(1);
    }

    boolean isEmpty() {
        return size==0;
    }

    void clear() {
        // Null out task references to prevent memory leak
        for (int i=1; i<=size; i++)
            queue[i] = null;

        size = 0;
    }

    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;
        }
    }

    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;
        }
    }

    void heapify() {
        for (int i = size/2; i >= 1; i--)
            fixDown(i);
    }
}

按照咱們以前介紹的二叉堆的相關知識,咱們能夠看到TimerQueue維護了TimerTask的數組queue,初始大小size爲0。

add操做首先判斷了數組是否滿了,若是數組已經滿了,那麼先執行擴容操做,再進行添加操做。如上所述,add操做先將元素放到二叉樹末尾的元素(queue[++size]),以後對這個元素進行上浮來維護堆序性。

getMin直接返回二叉樹的樹根(queue[1]),get方法直接返回數組的第i個元素。removeMin方法會將二叉樹末尾的元素(queue[size])移動到樹根(queue[1]),並將本來二叉樹末尾的元素設置成null,來讓垃圾回收器回收這個TimerTask,以後執行fixDown來維護堆序性,quickRemove也是相同的過程,只不過它在移動元素後沒有執行下沉操做,當連續執行屢次quickRemove後統一執行heapify來維護堆序性。

rescheduleMin會將樹根元素的元素值設置成newTime,並將它下沉到合適的位置。

fixUpfixDownheapify操做就如上文所述,用來維護二叉堆的讀序性。不過這裏面實現的fixUpfixDown並不優雅,基於交換臨位元素的實現須要使用T(3log(N))的時間,而實際上有T(log(N))的實現方法。後續的文章中會詳細介紹優先隊列與二叉堆的實現方式。

TimerThread


咱們直接來看TimerThread的代碼

class TimerThread extends Thread {
    boolean newTasksMayBeScheduled = true;

    private TaskQueue queue;

    TimerThread(TaskQueue queue) {
        this.queue = queue;
    }

    public void run() {
        try {
            mainLoop();
        } finally {
            // Someone killed this Thread, behave as if Timer cancelled
            synchronized(queue) {
                newTasksMayBeScheduled = false;
                queue.clear();  // Eliminate obsolete references
            }
        }
    }

    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) {
            }
        }
    }
}

首先是控制變量newTasksMayBeScheduled,表示當前工做線程是否應該繼續執行任務,當它爲false的時候它將不會再從任務隊列中取任務執行,表示當前工做線程已結束。接下來的queue變量是經過構造方法傳進來的任務隊列,工做線程的任務隊列與Timer共享,實現生產消費者模型。

進入到run()方法,run()方法會調用mainLoop()方法來執行主循環,而finally代碼塊會在主循環結束後清空任務隊列實現優雅退出。

mainLoop()方法中執行了死循環來拉取執行任務,在死循環中首先獲取queue的鎖來實現線程同步,接下來判斷任務隊列是否爲且工做線程是否中止,若是任務隊列爲空且工做線程未中止,那麼就使用queue.wait()來等待Timer添加任務後喚醒該線程,Object#wait()方法會釋放當前線程所持有的該對象的鎖,關於wait/notisfy的內容能夠去看Java API相關介紹。若是queue退出等待後依舊爲空,則表示newTasksMayBeScheduledfalse,工做線程已中止,退出主循環,不然會從任務隊列中取出須要最近執行的任務(並不會刪除任務)。

取到須要最近執行的任務後,獲取該任務的鎖,並判斷該任務是否已經中止,若是該任務已經中止,那麼就把它從任務隊列中移除,並什麼都不作繼續執行主循環。接下來判斷當前時間是否小於等於任務的下一次執行時間,若是知足條件則將taskFired設置成true,判斷當前任務是否須要重複執行。若是不須要重複執行就將它從任務隊列中移除,並將任務狀態設置成EXECUTED,若是須要重複執行就根據period設置它的下一次執行時間並從新調整任務隊列。

完成這些操做後,若是taskFiredfalse,就讓queue對象進入有限等待狀態,很容易獲得咱們須要的最大等待時間爲executionTime - currentTime。若是taskFiredtrue,那麼就釋放鎖並執行被取出的任務。

Timer


Timer對象的成員

首先來看Timer的成員部分

private final TaskQueue queue = new TaskQueue();
private final TimerThread thread = new TimerThread(queue);

private final Object threadReaper = new Object() {
    @SuppressWarnings("deprecation")
    protected void finalize() throws Throwable {
        synchronized(queue) {
            thread.newTasksMayBeScheduled = false;
            queue.notify(); // In case queue is empty.
        }
    }
};

private static final AtomicInteger nextSerialNumber = new AtomicInteger(0);

其中queue對象是如前面所說,爲了任務調度的最小優先隊列。接下來是TimerThread,它是Timer的工做線程,在Timer建立時就已經被分配,並與Timer共享任務隊列。

threadReaper是一個只複寫了finalize方法的對象,它的做用是當Timer對象沒有存活的引用後,終止任務線程,並等待任務隊列中的全部任務執行結束後退出工做線程,實現優雅退出。

nextSerialNumber用來記錄工做線程的序列號,全局惟一,避免生成的線程名稱衝突。

Timer對象的構造方法

接下來咱們看下Timer的全部構造方法

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

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

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

public Timer(String name, boolean isDaemon) {
    thread.setName(name);
    thread.setDaemon(isDaemon);
    thread.start();
}

能夠看到,全部的構造構造方法所作的事都相同:設置工做線程屬性,並啓動工做線程。

成員函數

接下來咱們能夠看下Timer的成員函數,咱們首先不考慮cancel()purge()方法,直接看schedule系列方法

public void schedule(TimerTask task, long delay) {
    if (delay < 0)
        throw new IllegalArgumentException("Negative delay.");
    sched(task, System.currentTimeMillis()+delay, 0);
}

public void schedule(TimerTask task, Date time) {
    sched(task, time.getTime(), 0);
}

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 schedule(TimerTask task, Date firstTime, long period) {
    if (period <= 0)
        throw new IllegalArgumentException("Non-positive period.");
    sched(task, firstTime.getTime(), -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);
}

public void scheduleAtFixedRate(TimerTask task, Date firstTime,
                                long period) {
    if (period <= 0)
        throw new IllegalArgumentException("Non-positive period.");
    sched(task, firstTime.getTime(), period);
}

能夠看到,全部的schedule方法除了作參數教研外,都將延遲時間和計劃執行時間轉化爲時間戳委託給sched方法來執行。schedulescheduleAtFixedRate傳遞的參數都相同,不過在傳遞period參數時使用符號來區分週期執行的方式。

接下來咱們能夠看下這位神祕嘉賓——sched方法到底作了哪些事

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

    // Constrain value of period sufficiently to prevent numeric
    // overflow while still being effectively infinitely large.
    if (Math.abs(period) > (Long.MAX_VALUE >> 1))
        period >>= 1;

    synchronized(queue) {
        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();
    }
}

sched方法首先作了一些參數校驗,保證期待執行時間不小於0,且執行週期不至於太大。接下來獲取任務隊列queue對象的monitor(監視器鎖),若是Timer的工做線程已經被中止了,那麼就會拋出IllegalStateException來禁止繼續添加任務,newTasksMayBeScheduled這個變量將會在稍後介紹。以後sched方法會嘗試獲取task.lock對象的鎖,判斷task的狀態避免重複添加,並設置task的下一次執行時間、task的執行週期和狀態。以後將task添加到任務隊列中,若是當前任務就是執行時間最近的任務,那麼就會喚起等待queue對象的線程(其實就是thread工做線程)繼續執行。

總結

本文從各個方面介紹了java.util.Timer類的使用方式,並從源碼角度介紹了java.util.Timer的實現方式。看完本文後,讀者應當掌握

  1. 如何執行晚於當前時間的任務
  2. 當任務執行時間早於當前時間會發生什麼
  3. 如何向Timer中添加多個任務
  4. 如何週期性執行任務
  5. 如何中止任務
  6. 如何本身實現相似的定時器

但願本文能夠幫助你們在工做中更好地使用java.util.Timer

相關文章
相關標籤/搜索