定時任務的執行服務

應用場景

(1) 鬧鐘程序或任務提醒,指定時間叫牀或在指定日期提醒還信用卡。

(2) 監控系統,每隔一段時間採集下系統數據,對異常事件報警

(3) 統計系統,一般凌晨一定時間統計昨日的各種數據指標。

實現定時任務的兩種方式

(1) 使用java.util包中的Timer和TimerTask。

(2) 使用Java併發包中的ScheduleExecutorService。

Timer和TimerTask

基本用法

TimerTask 表示一個定時任務,它是一個抽象類,實現了Runable,具體的定時任務需要繼承該類,實現了run方法。Timer是一個具體類,它負責定時任務的調度和執行,主要方法有:

//在當前時間延時delay 毫秒後運行任務task
public void schedule(TimerTask task, long delay) {
        if (delay < 0)
            throw new IllegalArgumentException("Negative delay.");
        sched(task, System.currentTimeMillis()+delay, 0);
    }

// 在指定絕對時間time 執行任務task
 public void schedule(TimerTask task, Date time) {
        sched(task, time.getTime(), 0);
    }

//固定延時重複執行,第一次計劃執行時間爲firstTime,後一次的計劃執行時間爲前一次“實際”執行時間加上period
public void schedule(TimerTask task, Date firstTime, long period) {
        if (period <= 0)
            throw new IllegalArgumentException("Non-positive period.");
        sched(task, firstTime.getTime(), -period);
    }

// 固定延時重複執行,第一次執行時間爲當前時間加上delay
 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);
    }

//固定頻率重複執行,第一次執行時間爲當前時間加上delay
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);
    }

//固定頻率重複執行,第一次計劃執行時間爲firstTime,後一次的計劃執行時間爲前一次“實際”執行時間加上period
public void scheduleAtFixedRate(TimerTask task, Date firstTime,
                                    long period) {
        if (period <= 0)
            throw new IllegalArgumentException("Non-positive period.");
        sched(task, firstTime.getTime(), period);
    }

需要注意固定延時與固定頻率的區別,二者都是重複執行,但後一次任務執行相對的時間是不一樣的,對於固定延時,它是基於上次任務的“實際”執行時間來算的,如果由於某種原因,上次任務延時了,則本次任務也會延時,而固定頻率會盡量補夠運行次數。

另外,需要注意的是,如果第一次計劃執行的時間firstTime是一個過去的時間,則任務會立即運行,對於固定延時的任務,下次任務會基於第一次執行時間計算,而對於固定頻率的任務,則會從firstTime開始算,有可能加上period後還是一個過去時間,從而連續運行很多次,直到時間超過當前時間。

基本示例

package com.claa.javabasic.TimerDemo;

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

/**
 * @Author: claa
 * @Date: 2020/05/24 08:58
 * @Description:
 */
public class BasicTimer {
    static class DelayTask extends TimerTask{

        @Override
        public void run() {
            System.out.println("delayed task");
        }
    }

    public static void main(String[] args) throws InterruptedException{
       Timer timer = new Timer();

       timer.schedule(new DelayTask(),1000);
       Thread.sleep(2000);
       timer.cancel();  // 取消所有定時任務
    }
}

package com.claa.javabasic.TimerDemo;

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

/**
 * @Author: claa
 * @Date: 2020/05/24 09:20
 * @Description: 固定延時示例
 * 有兩個定時任務,第一個運行一次,但耗時5秒,第二個是重複執行,1秒一次,
 * 第一個先運行,運行該程序,會發現,第二個任務只有在第一個任務運行結束後纔開始運行,運行後1秒1次
 */
public class TimerFixedDelay {
    static class LongRuningTask extends TimerTask {

        @Override
        public void run() {
            try{
                Thread.sleep(5000);
            }catch(InterruptedException e){
            }
            System.out.println("long runing finished");
        }
    }

    static class FixedDelayTask extends TimerTask{

        @Override
        public void run() {
            System.out.println(System.currentTimeMillis());
        }
    }

    public static void main(String[] args) throws InterruptedException{
        Timer timer = new Timer();

        timer.schedule(new LongRuningTask(),10);
        timer.schedule(new FixedDelayTask(),100,1000);
    }
}

package com.claa.javabasic.TimerDemo;

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

/**
 * @Author: claa
 * @Date: 2020/05/24 09:32
 * @Description:固定頻率
 * 第二個任務同樣只有在第一個任務運行結束後纔會運行,但它會把之前沒有運行的次數補過來,一下子運行5次
 */
public class TimerFixedRate {
    static class LongRuningTask extends TimerTask {

        @Override
        public void run() {
            try{
                Thread.sleep(5000);
            }catch(InterruptedException e){
            }
            System.out.println("long runing finished");
        }
    }

    static class FixedRateTask extends TimerTask {

        @Override
        public void run() {
            System.out.println(System.currentTimeMillis());
        }
    }

    public static void main(String[] args) throws InterruptedException{
        Timer timer = new Timer();

        timer.schedule(new LongRuningTask(),10);
        timer.scheduleAtFixedRate(new FixedRateTask(),100,1000);
    }
}

基本原理

Timer 內部主要由任務隊列和Timer線程兩部分組成。任務隊列是一個基於堆實現的優先級隊列,按照下次執行的時間安排優先級。Timer線程負責執行所有的定時任務,需要強調的是,一個Timer對象只有一個Timer線程,所以,上面的例子,任務會被延遲。

Timer線程主體是一個循環,從隊列中獲取任務,如果隊列中有任務且計劃執行時間小於等於當前時間 ,就執行它。如果隊列中沒有任務或第一個任務延時還沒到,就睡眠。如果睡眠過程中隊列上添加了新任務且新任務是第一個任務,Timer線程會被喚醒,重新進行檢查。

在執行任務之前,Timer線程判斷任務是否爲週期任務,如果是,就設置下次執行的時間並添加到優先級隊列,對於固定延時的任務,下次執行時間爲當前時間加上period, 對於固定頻率的任務,下次執行時間爲上次計劃執行時間加上period。

需要強調的是,下次任務的計劃是在執行當前任務之前就做出的,對於固定延時的任務,延時相對的是任務執行前的當前時間,而不是任務執行後,這與後面講到的ScheduledExecutorService 的固定延時計算方法 不同,後者更合乎一般的期望。對於固定頻率的任務 ,延時相對的是最先的計劃,所以,很有可能會出現前面例子中一下子執行很多次任務的情況。

死循環

一個Timer對象只有一個Timer線程,這意味着,定時任務不能耗時太長,更不能是無限循環。

package com.claa.javabasic.TimerDemo;

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

/**
 * @Author: claa
 * @Date: 2020/05/24 10:16
 * @Description:Timer 循環示例
 */
public class EndLessLoopTimer {
    static class LoopTask extends TimerTask{

        @Override
        public void run() {
            while(true){
                try{
                    // 模擬執行任務
                    Thread.sleep(1000);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
        }
    }

    // 永遠沒有機會執行
    static class ExampleTask extends TimerTask{

        @Override
        public void run() {
            System.out.println("hello");
        }
    }

    public static void main(String[] args)  throws InterruptedException{
        Timer timer  = new Timer();

        timer.schedule(new LoopTask(),10);
        timer.schedule(new ExampleTask(),100);
    }
}

異常處理

關於Timer線程,在執行任何一個任務的run方法時,一旦拋出異常,Timer線程就會退出,從而所有定時任務都會被取消。

package com.claa.javabasic.TimerDemo;

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

/**
 * @Author: claa
 * @Date: 2020/05/24 10:33
 * @Description:
 */
public class TimerException {
    static class TaskA extends TimerTask {

        @Override
        public void run() {
            System.out.println("task A");
        }
    }

    static class TaskB extends TimerTask{

        @Override
        public void run() {
            try{
            System.out.println("task B");
            throw new RuntimeException();
            }catch (RuntimeException  e){
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Timer timer = new Timer();

        timer.schedule(new TaskA(),1,1000);

        timer.schedule(new TaskB(),2000,1000);
    }
}

如果希望各個定時任務不互相干擾,一定要在run方法內捕獲所有異常。

注意點

(1)後臺只有一個線程在運行;

(2)) 固定頻率的任務被延遲後,可能會立即執行多次,將次數補夠;

(3)固定延時任務的延時相對的是任務執行前的時間;

(4))不要在定時任務中使用無限循環;

(5) 一個定時任務的未處理異常會導致所有定時任務被取消。

ScheduledExecutorService

基本用法

public interface ScheduledExecutorService extends ExecutorService {

 // 單次執行,在指定延時delay 後運行command
public ScheduledFuture<?> schedule(Runnable command,
                                       long delay, TimeUnit unit);

// 單次執行,在指定延時delay 後運行callable 
public <V> ScheduledFuture<V> schedule(Callable<V> callable,
                                           long delay, TimeUnit unit);
// 固定頻率重複執行                                         
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit);
   
// 固定延時重複執行
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit);                                                  
}

對於固定頻率的任務,第一次執行時間爲initialDelay後,第二次爲initialDelay+period,第三次爲initialDelay+2*period,以此類推。不過,對於固定延時的任務,它是從任務執行後開始算的,第一次爲initialDelay後,第二次爲第一次任務執行結束後再加上delay

ScheduledExecutorService的主要實現類是ScheduledThreadPoolExecutor,它是線程池ThreadPoolExecutor的子類,是基於線程池實現的,它的主要構造方法是:

public ScheduledThreadPoolExecutor(int corePoolSize)

此外,還有構造方法可以接受參數ThreadFactory和RejectedExecutionHandler。

它的任務隊列是一個無界的優先級隊列,所以最大線程數對它沒有作用,即使core-PoolSize設爲0,它也會至少運行一個線程。

基本示例

由於可以有多個線程執行定時任務,一般任務就不會被某個長時間運行的任務所延遲了。

package com.claa.javabasic.ScheduledDemo;

import java.util.TimerTask;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * @Author: claa
 * @Date: 2020/05/24 11:23
 * @Description:
 */
public class ScheduledFixedDelay {
    static class LongRuningTask extends TimerTask {

        @Override
        public void run() {
            try{
                Thread.sleep(5000);
            }catch(InterruptedException e){
            }
            System.out.println("long runing finished");
        }
    }

    static class FixedDelayTask extends TimerTask{

        @Override
        public void run() {
            System.out.println(System.currentTimeMillis());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ScheduledExecutorService timer = Executors.newScheduledThreadPool(10);

        timer.schedule(new LongRuningTask(),10, TimeUnit.MILLISECONDS);

        timer.scheduleWithFixedDelay(new FixedDelayTask(),100,1000,TimeUnit.MILLISECONDS);
    }
}

與Timer不同,單個定時任務的異常不會再導致整個定時任務被取消,即使後臺只有一個線程執行任務。

package com.claa.javabasic.ScheduledDemo;

import java.util.TimerTask;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * @Author: claa
 * @Date: 2020/05/24 11:31
 * @Description:
 */
public class ScheduleException {
    static class TaskA extends TimerTask {

        @Override
        public void run() {
            System.out.println("task A");
        }
    }

    static class TaskB extends TimerTask{

        @Override
        public void run() {
            try{
                System.out.println("task B");
                throw new RuntimeException();
            }catch (RuntimeException  e){
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ScheduledExecutorService timer = Executors.newSingleThreadScheduledExecutor();

        timer.scheduleWithFixedDelay(new TaskA(),0,1, TimeUnit.SECONDS);

        timer.scheduleWithFixedDelay(new TaskB(),2,1, TimeUnit.SECONDS);

    }

}

定時任務TaskB被取消了,但TaskA不受影響,即使它們是由同一個線程執行的。不過,需要強調的是,與Timer不同,沒有異常被拋出,TaskB的異常沒有在任何地方體現。所以,與Timer中的任務類似,應該捕獲所有異常。

基本原理

ScheduledThreadPoolExecutor的實現思路與Timer基本是類似的,都有一個基於堆的優先級隊列,保存待執行的定時任務,

它的主要不同是:

(1)它的背後是線程池,可以有多個線程執行任務。

(2)它在任務執行後再設置下次執行的時間,對於固定延時的任務更爲合理。

(3)任務執行線程會捕獲任務執行過程中的所有異常,一個定時任務的異常不會影響其他定時任務,不過,發生異常的任務(即使是一個重複任務)不會再被調度。

參考文章

java編程的邏輯基礎(馬俊昌)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章