深入瞭解Java定時器中的Timer的原理

Java在1.3版本引入瞭Timer工具類,它是一個古老的定時器,搭配TimerTask和TaskQueue一起使用。從Java5開始在並發包中引入瞭另一個定時器
ScheduledThreadPoolExecutor,它對Timer做瞭很多改進並提供瞭更多的工具,可以認為是對Timer的取代。

那為什麼還要介紹Timer工具類呢?通過瞭解Timer的功能和它背後的原理,有助於我們更好的對比瞭解
ScheduledThreadPoolExecutor,同時ScheduledThreadPoolExecutor的一些改進思想在我們平時的編碼工作中也可以借鑒。

主要成員變量

Timer中用到的主要是兩個成員變量:

  • TaskQueue:一個按照時間優先排序的隊列,這裡的時間是每個定時任務下一次執行的毫秒數(相對於1970年1月1日而言)
  • TimerThread:對TaskQueue裡面的定時任務進行編排和觸發執行,它是一個內部無限循環的線程。
//根據時間進行優先排序的隊列    
private final TaskQueue queue = new TaskQueue();

//消費線程,對queue中的定時任務進行編排和執行
private final TimerThread thread = new TimerThread(queue);

//構造函數
public Timer(String name) {
        thread.setName(name);
        thread.start();
}

定時功能

Timer提供瞭三種定時模式:

  • 一次性任務
  • 按照固定的延遲執行(fixed delay)
  • 按照固定的周期執行(fixed rate)

第一種比較好理解,即任務隻執行一次;針對第一種,Timer提供瞭以下兩個方法:

//在當前時間往後delay個毫秒開始執行
public void schedule(TimerTask task, long delay) {...}
//在指定的time時間點執行
public void schedule(TimerTask task, Date time) {...}

第二種Fixed Delay模式也提供瞭以下兩個方法

//從當前時間開始delay個毫秒數開始定期執行,周期是period個毫秒數
public void schedule(TimerTask task, long delay, long period) {...}
////從指定的firstTime開始定期執行,往後每次執行的周期是period個毫秒數
public void schedule(TimerTask task, Date firstTime, long period){...}

它的工作方式是:

第一次執行的時間將按照指定的時間點執行(如果此時TimerThread不在執行其他任務),如有其他任務在執行,那就需要等到其他任務執行完成才能執行。

從第二次開始,每次任務的執行時間是上一次任務開始執行的時間加上指定的period毫秒數。

如何理解呢,我們還是看代碼

public static void main(String[] args) {
        TimerTask task1 = new DemoTimerTask("Task1");
        TimerTask task2 = new DemoTimerTask("Task2");
        Timer timer = new Timer();
        timer.schedule(task1, 1000, 5000);
        timer.schedule(task2, 1000, 5000);
}
    
static class DemoTimerTask extends TimerTask {
        private String taskName;
        private DateFormat df = new SimpleDateFormat("HH:mm:ss---");
        
        public DemoTimerTask(String taskName) {
            this.taskName = taskName;
        }
        
        @Override
        public void run() {
            System.out.println(df.format(new Date()) + taskName + " is working.");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println(df.format(new Date()) + taskName + " finished work.");
        }
}

task1和task2是幾乎同時執行的兩個任務,而且執行時長都是2秒鐘,如果此時我們把第六行註掉不執行,我們將得到如下結果(和第三種Fixed Rate模式結果相同):

13:42:58—Task1 is working.
13:43:00—Task1 finished work.
13:43:03—Task1 is working.
13:43:05—Task1 finished work.
13:43:08—Task1 is working.
13:43:10—Task1 finished work.

如果打開第六行,我們再看下兩個任務的執行情況。我們是期望兩個任務能夠同時執行,但是Task2是在Task1執行完成後才開始執行(原因是TimerThread是單線程的,每個定時任務的執行也在該線程內完成,當多個任務同時需要執行時,隻能是阻塞瞭),從而導致Task2第二次執行的時間是它上一次執行的時間(13:43:57)加上5秒鐘(13:44:02)。

13:43:55—Task1 is working.
13:43:57—Task1 finished work.
13:43:57—Task2 is working.
13:43:59—Task2 finished work.
13:44:00—Task1 is working.
13:44:02—Task1 finished work.
13:44:02—Task2 is working.
13:44:04—Task2 finished work.

那如果此時還有個Task3也是同樣的時間點和間隔執行會怎麼樣呢?

結論是:也將依次排隊,執行的時間依賴兩個因素:

1.上次執行的時間

2.期望執行的時間點上有沒有其他任務在執行,有則隻能排隊瞭

我們接下來看下第三種Fixed Rate模式,我們將上面的代碼稍作修改:

public static void main(String[] args) {
        TimerTask task1 = new DemoTimerTask("Task1");
        TimerTask task2 = new DemoTimerTask("Task2");
        
        Timer timer = new Timer();
        timer.scheduleAtFixedRate(task1, 1000, 5000);
        timer.scheduleAtFixedRate(task2, 1000, 5000);
}
    
static class DemoTimerTask extends TimerTask {
        private String taskName;
        private DateFormat df = new SimpleDateFormat("HH:mm:ss---");
        
        public DemoTimerTask(String taskName) {
            this.taskName = taskName;
        }
        
        @Override
        public void run() {
            System.out.println(df.format(new Date()) + taskName + " is working.");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println(df.format(new Date()) + taskName + " finished work.");
        }
}

Task1和Task2還是在相同的時間點,按照相同的周期定時執行任務,我們期望Task1能夠每5秒定時執行任務,期望的時間點是:14:21:47-14:21:52-14:21:57-14:22:02-14:22:07,實際上它能夠交替著定期執行,原因是Task2也會定期執行,並且對TaskQueue的鎖他們是交替著拿的(這個在下面分析TimerThread源碼的時候會講到)

14:21:47—Task1 is working.
14:21:49—Task1 finished work.
14:21:49—Task2 is working.
14:21:51—Task2 finished work.
14:21:52—Task2 is working.
14:21:54—Task2 finished work.
14:21:54—Task1 is working.
14:21:56—Task1 finished work.
14:21:57—Task1 is working.
14:21:59—Task1 finished work.
14:21:59—Task2 is working.
14:22:01—Task2 finished work.

TimerThread

上面我們主要講瞭Timer的一些主要源碼及定時模式,下面我們來分析下支撐Timer的定時任務線程TimerThread。

TimerThread大概流程圖如下:

TimerThread流程

源碼解釋如下:

private void mainLoop() {
        while (true) {
            try {
                TimerTask task;
                boolean taskFired;
                synchronized(queue) {
                    // 如果queue裡面沒有要執行的任務,則掛起TimerThread線程
                    while (queue.isEmpty() && newTasksMayBeScheduled)
                        queue.wait();
                    // 如果TimerThread被激活,queue裡面還是沒有任務,則介紹該線程的無限循環,不再接受新任務
                    if (queue.isEmpty())
                        break; 

                    long currentTime, executionTime;
                    // 獲取queue隊列裡面下一個要執行的任務(根據時間排序,也就是接下來最近要執行的任務)
                    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;
                        // taskFired表示是否需要立刻執行線程,當task的下次執行時間到達當前時間點時為true
                        if (taskFired = (executionTime<=currentTime)) {
                            //task.period==0表示這個任務隻需要執行一次,這裡就從queue裡面刪掉瞭
                            if (task.period == 0) { 
                                queue.removeMin();
                                task.state = TimerTask.EXECUTED;
                            } else { // Repeating task, reschedule
                                //針對task.period不等於0的任務,則計算它的下次執行時間點
                                //task.period<0表示是fixed delay模式的任務
                                //task.period>0表示是fixed rate模式的任務
                                queue.rescheduleMin(
                                  task.period<0 ? currentTime   - task.period
                                                : executionTime + task.period);
                            }
                        }
                    }
                    // 如果任務的下次執行時間還沒有到達,則掛起TimerThread線程executionTime - currentTime毫秒數,到達執行時間點再自動激活
                    if (!taskFired) 
                        queue.wait(executionTime - currentTime);
                }
                // 如果任務的下次執行時間到瞭,則執行任務
                // 註意:這裡任務執行沒有另起線程,還是在TimerThread線程執行的,所以當有任務在同時執行時會出現阻塞
                if (taskFired)  
                    // 這裡沒有try catch異常,當TimerTask拋出異常會導致整個TimerThread跳出循環,從而導致Timer失效
                    task.run();
            } catch(InterruptedException e) {
            }
        }
}

結論

通過上面的分析,我們可以得出以下結論:

  • Timer支持三種模式的定時任務(一次性任務,Fixed Delay模式,Fixed Rate模式)
  • Timer中的TimerThread是單線程模式,因此導致所有定時任務不能同時執行,可能會出現延遲
  • TimerThread中並沒有處理好任務的異常,因此每個TimerTask的實現必須自己try catch防止異常拋出,導致Timer整體失效

Demo代碼位置

TimerFixedRateDemo.java

TimerFixedDelayDemo.java

到此這篇關於深入瞭解Java定時器中的Timer的原理的文章就介紹到這瞭,更多相關Java Timer原理內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: