1. 程式人生 > 實用技巧 >Java定時任務-Timer

Java定時任務-Timer

1. Timer簡單使用

簡單使用:

public class TimerTest {
    public static void main(String[] args) throws InterruptedException {
        // 建立Timer物件
        Timer timer = new Timer();
        // 延遲1秒執行任務,只執行一次。
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        }, 1000);
        Thread.sleep(2000);
        timer.cancel(); // 取消任務執行,如果沒有呼叫cancel,timer執行緒不會結束。
    }
}

TimerTask介面繼承了Runnable,重寫裡面的run方法就可以自定義任務。

2. Timer

Timer類中,有兩個重要欄位:

/**
* The timer task queue.  This data structure is shared with the timer
* thread.  The timer produces tasks, via its various schedule calls,
* and the timer thread consumes, executing timer tasks as appropriate,
* and removing them from the queue when they're obsolete.
*/
private final TaskQueue queue = new TaskQueue();
/**
* The timer thread.
*/
private final TimerThread thread = new TimerThread(queue);

TaskQueue用來儲存提交的定時任務的佇列;TimerThread是Timer的內部類,繼承了Thread類。

TaskQueue的底層實現是陣列,它是一個優先佇列。

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

優先順序參考下一次執行時間(每一個TimerTask下一次的執行時間),越快該執行的任務就會排的越靠前。

TimerThread是一個執行緒,當Timer建立時,該執行緒就被啟動:

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

3. 配置定時任務

主要用Timer中的schedule方法來配置不同的定時任務。

  • schedule(TimerTask task, long delay):在delay(毫秒)延遲後,執行任務task。只執行一次。
  • schedule(TimerTask task, Date time):在指定的時間點(time)執行task。如果時間點是過去某個時刻,那麼該任務將被立即執行。
  • schedule(TimerTask task, long delay, long period):在當前時間delay(毫秒)延遲後執行task,然後每隔period(毫秒)執行一次
  • schedule(TimerTask task, Date firstTime, long period):在指定時間firstTime執行第一次task,然後每隔period(毫秒)執行一次。
  • scheduleAtFixedRate(TimerTask task, long delay, long period):在delay延遲後執行task,每隔period執行一次
  • scheduleAtFixedRate(TimerTask task, Date firstTime, long period):在指定時間執行task,每隔period執行一次

schedule(TimerTask task, long delay, long period) 與 scheduleAtFixedRate(TimerTask task, long delay, long period)的區別:
兩者都是在當前時間的delay延遲後,執行第一次任務,並且之後每隔period執行一次。

區別在於:當任務的執行時間 > 任務間間隔時間(period)時,

  • schedule方法會阻塞,直到上一個任務執行完畢後,再執行下一個任務;
  • scheduleAtFixedRate方法不會阻塞,上一個任務即使未執行完畢,只要間隔時間足夠period,就執行下一個任務。

在演示前,需要搞明白兩個時間變數:
系統時間:正常流逝的時間(System.currentTimeMillis()或new Date())。
任務被執行的時間(TimerTask中的scheduledExecutionTime()方法)。

示例:schedule

public static void main(String[] args) throws InterruptedException {
    Timer timer = new Timer();
    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    timer.schedule(new TimerTask() {
        @Override
        public void run() {
            // 我們在任務中分別列印“任務被執行時間”和當前系統時間
            System.out.println("scheuledExecutionTime : " + dateFormat.format(new Date(this.scheduledExecutionTime())));
            System.out.println("new Date() : " + dateFormat.format(new Date()));
            try {
                // 阻塞6秒,超過了任務間隔4秒
                Thread.sleep(6000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }, 2000, 4000); // 指定任務執行間隔4秒
}

列印結果:

scheuledExecutionTime : 2020-08-23 13:49:33
new Date() : 2020-08-23 13:49:33
scheuledExecutionTime : 2020-08-23 13:49:39
new Date() : 2020-08-23 13:49:39
scheuledExecutionTime : 2020-08-23 13:49:45
new Date() : 2020-08-23 13:49:45
scheuledExecutionTime : 2020-08-23 13:49:51
new Date() : 2020-08-23 13:49:51
scheuledExecutionTime : 2020-08-23 13:49:57
new Date() : 2020-08-23 13:49:57

可以看到,任務執行時間(scheduleExecutionTime)和系統時間的間隔都是6秒。
任務執行的實際間隔是6秒,也就是上一個任務結束後,下一個任務才執行,並未按照設定的4秒間隔。

示例:scheduleAtFixedRate

 public static void main(String[] args) throws InterruptedException {
     Timer timer = new Timer();
     SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
     timer.scheduleAtFixedRate(new TimerTask() {
         @Override
         public void run() {
             System.out.println("scheuledExecutionTime : " + dateFormat.format(new Date(this.scheduledExecutionTime())));
             System.out.println("new Date() : " + dateFormat.format(new Date()));
             try {
                 Thread.sleep(6000);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
     }, 2000, 4000);
}

列印結果:

scheuledExecutionTime : 2020-08-23 14:31:54
new Date() : 2020-08-23 14:31:54
scheuledExecutionTime : 2020-08-23 14:31:58
new Date() : 2020-08-23 14:32:00
scheuledExecutionTime : 2020-08-23 14:32:02
new Date() : 2020-08-23 14:32:06
scheuledExecutionTime : 2020-08-23 14:32:06
new Date() : 2020-08-23 14:32:12
scheuledExecutionTime : 2020-08-23 14:32:10
new Date() : 2020-08-23 14:32:18

可以看到:任務被執行的時間(scheduleExecutionTime)的間隔是4秒,而列印的系統時間間隔是6秒。也就是說,任務的執行時間間隔按照設定的4秒進行,並未因上一個任務的阻塞而導致間隔延長。
但是注意:“scheuledExecutionTime”和“new Date() ”兩句話都是同時列印的,都是在一個task開始時被列印。

schedule(TimerTask task, Date firstTime, long period) 與 scheduleAtFixedRate(TimerTask task, Date firstTime, long period)的區別:
這兩個方法的區別主要在於:當firstTime早於當前系統時間時,執行的情況不同。

  • schedule:會立即執行task一次,之後每隔period執行一次;
  • scheduleAtFixedRate:會將從firstTime到當前系統時間(System.currentTimeMillis())之間應該執行的任務一次性執行完畢,然後繼續按照period間隔執行。

示例:schedule

public static void main(String[] args) throws InterruptedException {
    Timer timer = new Timer();
    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    // 當前時間
    System.out.printf("=============當前時間: %s=================\n", dateFormat.format(new Date()));
    // 從當前時間的20秒前開始執行任務
    Date startDate = Date.from(Instant.now().minusSeconds(20));
    timer.schedule(new TimerTask() {
        @Override
        public void run() {
            System.out.println("scheuledExecutionTime : " + dateFormat.format(new Date(this.scheduledExecutionTime())));
        }
    }, startDate, 4000); // 每隔4秒執行一次
}

列印結果:

=============當前時間: 2020-08-23 14:45:05=================
scheuledExecutionTime : 2020-08-23 14:45:05
scheuledExecutionTime : 2020-08-23 14:45:09
scheuledExecutionTime : 2020-08-23 14:45:13
scheuledExecutionTime : 2020-08-23 14:45:17
scheuledExecutionTime : 2020-08-23 14:45:21

可以看到,任務是從當前時間開始執行的,並未從startDate這個時間點開始。

示例:scheduleAtFixedRate

public static void main(String[] args) throws InterruptedException {
     Timer timer = new Timer();
     SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
     // 當前時間
     System.out.printf("=============當前時間: %s=================\n", dateFormat.format(new Date()));
     // 從當前時間的20秒前開始執行任務
     Date startDate = Date.from(Instant.now().minusSeconds(20));
     timer.scheduleAtFixedRate(new TimerTask() {
         @Override
         public void run() {
             System.out.println("scheuledExecutionTime : " + dateFormat.format(new Date(this.scheduledExecutionTime())));
         }
     }, startDate, 4000);
 }

列印結果:

=============當前時間: 2020-08-23 14:47:30=================
scheuledExecutionTime : 2020-08-23 14:47:10
scheuledExecutionTime : 2020-08-23 14:47:14
scheuledExecutionTime : 2020-08-23 14:47:18
scheuledExecutionTime : 2020-08-23 14:47:22
scheuledExecutionTime : 2020-08-23 14:47:26
scheuledExecutionTime : 2020-08-23 14:47:30
scheuledExecutionTime : 2020-08-23 14:47:34
scheuledExecutionTime : 2020-08-23 14:47:38
scheuledExecutionTime : 2020-08-23 14:47:42

可以看到:scheduleAtFixedRate方法將從開始時間(startDate 14:47:10)到當前時間之間(new Date() 14:47:30)之間的所有任務都執行一遍(這些任務都是同時執行,一次性輸出的),然後從當前時間起繼續以4秒為間隔往下執行。

4. 底層實現

每一個schedule方法的底層都是呼叫了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();
    }
}

該方法接收3個引數:

  • task:執行的任務;
  • time:任務下一次要執行的時間點;
  • period:執行任務的時間間隔。

當我們構造 Timer 例項的時候,就會啟動該執行緒,該執行緒會在一個死迴圈中嘗試從任務佇列上獲取任務,如果成功獲取就執行該任務並在執行結束之後做一個判斷。

如果 period 值為零,則說明這是一次普通任務,執行結束後將從佇列首部移除該任務。

如果 period 為負值,則說明這是一次固定延時的任務,修改它下次執行時間 nextExecutionTime 為當前時間減去 period,重構任務佇列。

如果 period 為正數,則說明這是一次固定頻率的任務,修改它下次執行時間為 上次執行時間加上 period,並重構任務佇列。

  1. Timer的不足
    Timer是單執行緒的,不管任務多少,僅有一個工作執行緒;
    限於單執行緒,如果第一個任務邏輯上死迴圈了,後續的任務一個都得不到執行。
    依然是由於單執行緒,任一任務丟擲異常後,整個 Timer 就會結束,後續任務全部都無法執行。

參考部落格:https://www.cnblogs.com/yangming1996/p/10317949.html