1. 程式人生 > >java Timer(定時呼叫、實現固定時間執行)

java Timer(定時呼叫、實現固定時間執行)

最近需要用到定時呼叫的功能。可以通過java的Timer類來進行定時呼叫,下面是有關Timer的一些相關知識。

其實就Timer來講就是一個排程器,而TimerTask呢只是一個實現了run方法的一個類,而具體的TimerTask需要由你自己來實現,例如這樣:

Timer timer = new Timer();
timer.schedule(new TimerTask() {
        public void run() {
            System.out.println("11232");
        }
}, 200000 , 1000);

這裡直接實現一個TimerTask(當然,你可以實現多個TimerTask,多個TimerTask可以被一個Timer會被分配到多個 Timer中被排程,後面會說到Timer的實現機制就是說內部的排程機制),然後編寫run方法,20s後開始執行,每秒執行一次,當然你通過一個 timer物件來操作多個timerTask,其實timerTask本身沒什麼意義,只是和timer集合操作的一個物件,實現它就必然有對應的run 方法,以被呼叫,他甚至於根本不需要實現Runnable,因為這樣往往混淆視聽了,為什麼呢?也是本文要說的重點。
  在說到timer的原理時,我們先看看Timer裡面的一些常見方法:

1、這個方法是排程一個task,經過delay(ms)後開始進行排程,僅僅排程一次。

 public void schedule(TimerTask task, long delay)

2、在指定的時間點time上排程一次。

public void schedule(TimerTask task, Date time)

3、這個方法是排程一個task,在delay(ms)後開始排程,每次排程完後,最少等待period(ms)後才開始排程。

public void schedule(TimerTask task, long delay, long period)

4、和上一個方法類似,唯一的區別就是傳入的第二個引數為第一次排程的時間。

public void schedule(TimerTask task, Date firstTime, long period)

5、排程一個task,在delay(ms)後開始排程,然後每經過period(ms)再次排程,貌似和方法:schedule是一樣的,其實不然,後面你會根據原始碼看到,schedule在計算下一次執行的時間的時候,是通過當前時間(在任務執行前得到) + 時間片,而scheduleAtFixedRate方法是通過當前需要執行的時間(也就是計算出現在應該執行的時間)+ 時間片,前者是執行的實際時間,而後者是理論時間點,例如:schedule時間片是5s,那麼理論上會在5、10、15、20這些時間片被排程,但是如果由於某些CPU徵用導致未被排程,假如等到第8s才被第一次排程,那麼schedule方法計算出來的下一次時間應該是第13s而不是第10s,這樣有可能下次就越到20s後而被少排程一次或多次,而scheduleAtFixedRate方法就是每次理論計算出下一次需要排程的時間用以排序,若第8s被排程,那麼計算出應該是第10s,所以它距離當前時間是2s,那麼再排程佇列排序中,會被優先排程,那麼就儘量減少漏掉排程的情況。

public void scheduleAtFixedRate(TimerTask task, long delay, long period)

6、方法同上,唯一的區別就是第一次排程時間設定為一個Date時間,而不是當前時間的一個時間片,我們在原始碼中會詳細說明這些內容。

public void scheduleAtFixedRate(TimerTask task, Date firstTime,long period)

首先看Timer的構造方法有幾種:
構造方法1:無參構造方法,簡單通過Tiemer為字首構造一個執行緒名稱:

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

建立的執行緒不為主執行緒,則主執行緒結束後,timer自動結束,而無需使用cancel來完成對timer的結束。

構造方法2:傳入了是否為後臺執行緒,後臺執行緒當且僅當程序結束時,自動登出掉。

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

 另外兩個構造方法負責傳入名稱和將timer啟動:

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

這裡有一個thread,這個thread很明顯是一個執行緒,被包裝在了Timer類中,我們看下這個thread的定義是:

private TimerThread thread = new TimerThread(queue);

  而定義TimerThread部分的是:
看到這裡知道了,Timer內部包裝了一個執行緒,用來做獨立於外部執行緒的排程,而TimerThread是一個default型別的,預設情況下是引用不到的,是被Timer自己所使用的。

  接下來看下有那些屬性
  除了上面提到的thread,還有一個很重要的屬性是:

private TaskQueue queue = new TaskQueue();

 看名字就知道是一個佇列,佇列裡面可以先猜猜看是什麼,那麼大概應該是我要排程的任務吧,先記錄下了,接下來繼續向下看:

  裡面還有一個屬性是:threadReaper, 它是Object型別,只是重寫了finalize方法而已,是為了垃圾回收的時候,將相應的資訊回收掉,做GC的回補,也就是當timer執行緒由於某種 原因死掉了,而未被cancel,裡面的佇列中的資訊需要清空掉,不過我們通常是不會考慮這個方法的,所以知道java寫這個方法是幹什麼的就行了。

  接下來看排程方法的實現:
  對於上面6個排程方法,我們不做一一列舉,為什麼等下你就知道了:
  來看下方法:

public void schedule(TimerTask task, long delay)

的原始碼如下:

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

這裡呼叫了另一個方法,將task傳入,第一個引數傳入System.currentTimeMillis()+delay可見為第一次需要執行的時間的 時間點了(如果傳入Date,就是物件.getTime()即可,所以傳入Date的幾個方法就不用多說了),而第三個引數傳入了0,這裡可以猜下要麼是 時間片,要麼是次數啥的,不過等會就知道是什麼了;另外關於方法:sched的內容我們不著急去看他,先看下過載的方法中是如何做的

  再看看方法:

public void schedule(TimerTask task, long delay,long period)

原始碼為:

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

 看來也呼叫了方法sched來完成排程,和上面的方法唯一的排程時候的區別是增加了傳入的period,而第一個傳入的是0,所以確定這個引數為時間片, 而不是次數,注意這個裡的period加了一個負數,也就是取反,也就是我們開始傳入1000,在呼叫sched的時候會變成-1000,其實最終閱讀完 原始碼後你會發現這個算是老外對於一種數字的理解,而並非有什麼特殊的意義,所以閱讀原始碼的時候也有這些困難所在。

  最後再看個方法是:

public void scheduleAtFixedRate(TimerTasktask,long delay,long 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);
   }

唯一的區別就是在period沒有取反,其實你最終閱讀完原始碼,上面的取反沒有什麼特殊的意義,老外不想增加一個引數來表示 scheduleAtFixedRate,而scheduleAtFixedRate和schedule的大部分邏輯程式碼一致,因此用了引數的範圍來作為 區分方法,也就是當你傳入的引數不是正數的時候,你呼叫schedule方法正好是得到scheduleAtFixedRate的功能,而呼叫 scheduleAtFixedRate方法的時候得到的正好是schedule方法的功能,呵呵,這些討論沒什麼意義,討論實質和重點:

  來看sched方法的實現體:

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

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

queue為一個佇列,我們先不看他資料結構,看到他在做這個操作的時候,發生了同步,所以在timer級別,這個是執行緒安全的,最後將task相關的引數賦值,主要包含nextExecutionTime(下一次執行時間),period(時間片),state(狀態),然後將它放入queue佇列中,做一次notify操作,為什麼要做notify操作呢?看了後面的程式碼你就知道了。

  簡言之,這裡就是講task放入佇列queue的過程,此時,你可能對queue的結構有些興趣,那麼我們先來看看queue屬性的結構TaskQueue:

class TaskQueue {

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

    private int size = 0;

可見,TaskQueue的結構很簡單,為一個數組,加一個size,有點像ArrayList,是不是長度就128呢,當然不 是,ArrayList可以擴容,它可以,只是會造成記憶體拷貝而已,所以一個Timer來講,只要內部的task個數不超過128是不會造成擴容的;內部 提供了add(TimerTask)、size()、getMin()、get(int)、removeMin()、quickRemove(int)、 rescheduleMin(long newTime)、isEmpty()、clear()、fixUp()、fixDown()、heapify();

實踐部分:

1、通過繼承TimerTask的方式實現
  必須重寫run方法.

public class MyTask extends TimerTask
{

    @Override
    public void run()
    {
        SimpleDateFormat sdf = null;
        sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
        System.out.println("當前時間:" + sdf.format(new Date()));

    }

}

{
    public static void main(String[] args)
    {
        Timer t = new Timer(); // 建立Timer物件
        MyTask task = new MyTask(); //定義任務
        t.schedule(task, 1000,2000);//設定任務的執行,1秒後開始,每2秒執行一次

        Calendar cal = Calendar.getInstance();
        cal.set(Calendar.MINUTE, 30);

        t.schedule(task, cal.getTime() , 2000);


    }
}

2、通過匿名內部類實現

Timer timer = new Timer();  
        timer.scheduleAtFixedRate(new TimerTask() {  
                public void run() {  

                    System.out.println("abc");  
                }  
        }, 1000 , 1000);

  最近需要用到定時呼叫的功能。可以通過java的Timer類來進行定時呼叫,下面是有關Timer的一些相關知識。

  其實就Timer來講就是一個排程器,而TimerTask呢只是一個實現了run方法的一個類,而具體的TimerTask需要由你自己來實現,例如這樣:

Timer timer = new Timer();
timer.schedule(new TimerTask() {
        public void run() {
            System.out.println("11232");
        }
}, 200000 , 1000);

  這裡直接實現一個TimerTask(當然,你可以實現多個TimerTask,多個TimerTask可以被一個Timer會被分配到多個 Timer中被排程,後面會說到Timer的實現機制就是說內部的排程機制),然後編寫run方法,20s後開始執行,每秒執行一次,當然你通過一個 timer物件來操作多個timerTask,其實timerTask本身沒什麼意義,只是和timer集合操作的一個物件,實現它就必然有對應的run 方法,以被呼叫,他甚至於根本不需要實現Runnable,因為這樣往往混淆視聽了,為什麼呢?也是本文要說的重點。
在說到timer的原理時,我們先看看Timer裡面的一些常見方法:

1、這個方法是排程一個task,經過delay(ms)後開始進行排程,僅僅排程一次。

    public void schedule(TimerTask task, long delay)

2、在指定的時間點time上排程一次。

public void schedule(TimerTask task, Date time)

3、這個方法是排程一個task,在delay(ms)後開始排程,每次排程完後,最少等待period(ms)後才開始排程。

public void schedule(TimerTask task, long delay, long period)

4、和上一個方法類似,唯一的區別就是傳入的第二個引數為第一次排程的時間。

public void schedule(TimerTask task, Date firstTime, long period)

5、排程一個task,在delay(ms)後開始排程,然後每經過period(ms)再次排程,貌似和方法:schedule是一樣的,其實不然,後面你會根據原始碼看到,schedule在計算下一次執行的時間的時候,是通過當前時間(在任務執行前得到) + 時間片,而scheduleAtFixedRate方法是通過當前需要執行的時間(也就是計算出現在應該執行的時間)+ 時間片,前者是執行的實際時間,而後者是理論時間點,例如:schedule時間片是5s,那麼理論上會在5、10、15、20這些時間片被排程,但是如果由於某些CPU徵用導致未被排程,假如等到第8s才被第一次排程,那麼schedule方法計算出來的下一次時間應該是第13s而不是第10s,這樣有可能下次就越到20s後而被少排程一次或多次,而scheduleAtFixedRate方法就是每次理論計算出下一次需要排程的時間用以排序,若第8s被排程,那麼計算出應該是第10s,所以它距離當前時間是2s,那麼再排程佇列排序中,會被優先排程,那麼就儘量減少漏掉排程的情況。

public void scheduleAtFixedRate(TimerTask task, long delay, long period)

6、方法同上,唯一的區別就是第一次排程時間設定為一個Date時間,而不是當前時間的一個時間片,我們在原始碼中會詳細說明這些內容。

public void scheduleAtFixedRate(TimerTask task, Date firstTime,long period)

首先看Timer的構造方法有幾種:
構造方法1:無參構造方法,簡單通過Tiemer為字首構造一個執行緒名稱:

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

建立的執行緒不為主執行緒,則主執行緒結束後,timer自動結束,而無需使用cancel來完成對timer的結束。

構造方法2:傳入了是否為後臺執行緒,後臺執行緒當且僅當程序結束時,自動登出掉。

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

另外兩個構造方法負責傳入名稱和將timer啟動:

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

這裡有一個thread,這個thread很明顯是一個執行緒,被包裝在了Timer類中,我們看下這個thread的定義是:

private TimerThread thread = new TimerThread(queue);

  而定義TimerThread部分的是:

看到這裡知道了,Timer內部包裝了一個執行緒,用來做獨立於外部執行緒的排程,而TimerThread是一個default型別的,預設情況下是引用不到的,是被Timer自己所使用的。

  接下來看下有那些屬性
  除了上面提到的thread,還有一個很重要的屬性是:

private TaskQueue queue = new TaskQueue();

 看名字就知道是一個佇列,佇列裡面可以先猜猜看是什麼,那麼大概應該是我要排程的任務吧,先記錄下了,接下來繼續向下看:

  裡面還有一個屬性是:threadReaper, 它是Object型別,只是重寫了finalize方法而已,是為了垃圾回收的時候,將相應的資訊回收掉,做GC的回補,也就是當timer執行緒由於某種 原因死掉了,而未被cancel,裡面的佇列中的資訊需要清空掉,不過我們通常是不會考慮這個方法的,所以知道java寫這個方法是幹什麼的就行了。

  接下來看排程方法的實現:
  對於上面6個排程方法,我們不做一一列舉,為什麼等下你就知道了:
  來看下方法:

public void schedule(TimerTask task, long delay)

的原始碼如下:

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

這裡呼叫了另一個方法,將task傳入,第一個引數傳入System.currentTimeMillis()+delay可見為第一次需要執行的時間的 時間點了(如果傳入Date,就是物件.getTime()即可,所以傳入Date的幾個方法就不用多說了),而第三個引數傳入了0,這裡可以猜下要麼是 時間片,要麼是次數啥的,不過等會就知道是什麼了;另外關於方法:sched的內容我們不著急去看他,先看下過載的方法中是如何做的

  再看看方法:

public void schedule(TimerTask task, long delay,long period)

 原始碼為:

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

看來也呼叫了方法sched來完成排程,和上面的方法唯一的排程時候的區別是增加了傳入的period,而第一個傳入的是0,所以確定這個引數為時間片, 而不是次數,注意這個裡的period加了一個負數,也就是取反,也就是我們開始傳入1000,在呼叫sched的時候會變成-1000,其實最終閱讀完 原始碼後你會發現這個算是老外對於一種數字的理解,而並非有什麼特殊的意義,所以閱讀原始碼的時候也有這些困難所在。

最後再看個方法是:

public void scheduleAtFixedRate(TimerTasktask,long delay,long 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);
   }

唯一的區別就是在period沒有取反,其實你最終閱讀完原始碼,上面的取反沒有什麼特殊的意義,老外不想增加一個引數來表示 scheduleAtFixedRate,而scheduleAtFixedRate和schedule的大部分邏輯程式碼一致,因此用了引數的範圍來作為 區分方法,也就是當你傳入的引數不是正數的時候,你呼叫schedule方法正好是得到scheduleAtFixedRate的功能,而呼叫 scheduleAtFixedRate方法的時候得到的正好是schedule方法的功能,呵呵,這些討論沒什麼意義,討論實質和重點:

  來看sched方法的實現體:

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

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

queue為一個佇列,我們先不看他資料結構,看到他在做這個操作的時候,發生了同步,所以在timer級別,這個是執行緒安全的,最後將task相關的引數賦值,主要包含nextExecutionTime(下一次執行時間),period(時間片),state(狀態),然後將它放入queue佇列中,做一次notify操作,為什麼要做notify操作呢?看了後面的程式碼你就知道了。

  簡言之,這裡就是講task放入佇列queue的過程,此時,你可能對queue的結構有些興趣,那麼我們先來看看queue屬性的結構TaskQueue:

class TaskQueue {

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

    private int size = 0;

可見,TaskQueue的結構很簡單,為一個數組,加一個size,有點像ArrayList,是不是長度就128呢,當然不 是,ArrayList可以擴容,它可以,只是會造成記憶體拷貝而已,所以一個Timer來講,只要內部的task個數不超過128是不會造成擴容的;內部 提供了add(TimerTask)、size()、getMin()、get(int)、removeMin()、quickRemove(int)、 rescheduleMin(long newTime)、isEmpty()、clear()、fixUp()、fixDown()、heapify();

實踐部分:

1、通過繼承TimerTask的方式實現
  必須重寫run方法.

public class MyTask extends TimerTask
{

    @Override
    public void run()
    {
        SimpleDateFormat sdf = null;
        sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
        System.out.println("當前時間:" + sdf.format(new Date()));

    }

}


public class TestTask
{
    public static void main(String[] args)
    {
        Timer t = new Timer(); // 建立Timer物件
        MyTask task = new MyTask(); //定義任務
        t.schedule(task, 1000,2000);//設定任務的執行,1秒後開始,每2秒執行一次

        Calendar cal = Calendar.getInstance();
        cal.set(Calendar.MINUTE, 30);

        t.schedule(task, cal.getTime() , 2000);


    }
}

2、通過匿名內部類實現

Timer timer = new Timer();  
        timer.scheduleAtFixedRate(new TimerTask() {  
                public void run() {  

                    System.out.println("abc");  
                }  
        }, 1000 , 1000);