Java中定時任務的6種實現方式

前言:

幾乎在所有的項目中,定時任務的使用都是不可或缺的,如果使用不當甚至會造成資損。還記得多年前在做金融系統時,出款業務是通過定時任務對外打款,當時由於銀行接口處理能力有限,外加定時任務使用不當,導致發出大量重復出款請求。還好在後面環節將交易卡在瞭系統內部,未發生資損。

所以,系統的學習一下定時任務,是非常有必要的。這篇文章就帶大傢整體梳理學習一下Java領域中常見的幾種定時任務實現。

1、線程等待實現

先從最原始最簡單的方式來講解。可以先創建一個thread,然後讓它在while循環裡一直運行著,通過sleep方法來達到定時任務的效果。

public class Task {

    public static void main(String[] args) {
        // run in a second
        final long timeInterval = 1000;
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("Hello !!");
                    try {
                        Thread.sleep(timeInterval);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        Thread thread = new Thread(runnable);
        thread.start();
    }
}

這種方式簡單直接,但是能夠實現的功能有限,而且需要自己來實現。

2、JDK自帶Timer實現

目前來看,JDK自帶的Timer API算是最古老的定時任務實現方式瞭。Timer是一種定時器工具,用來在一個後臺線程計劃執行指定任務。它可以安排任務“執行一次”或者定期“執行多次”。

在實際的開發當中,經常需要一些周期性的操作,比如每5分鐘執行某一操作等。對於這樣的操作最方便、高效的實現方式就是使用java.util.Timer工具類。

2.1 核心方法

imer類的核心方法如下:

// 在指定延遲時間後執行指定的任務
schedule(TimerTask task,long delay);

// 在指定時間執行指定的任務。(隻執行一次)
schedule(TimerTask task, Date time);

// 延遲指定時間(delay)之後,開始以指定的間隔(period)重復執行指定的任務
schedule(TimerTask task,long delay,long period);

// 在指定的時間開始按照指定的間隔(period)重復執行指定的任務
schedule(TimerTask task, Date firstTime , long period);

// 在指定的時間開始進行重復的固定速率執行任務
scheduleAtFixedRate(TimerTask task,Date firstTime,long period);

// 在指定的延遲後開始進行重復的固定速率執行任務
scheduleAtFixedRate(TimerTask task,long delay,long period);

// 終止此計時器,丟棄所有當前已安排的任務。
cancal();

// 從此計時器的任務隊列中移除所有已取消的任務。
purge();

2.2使用示例

下面用幾個示例演示一下核心方法的使用。首先定義一個通用的TimerTask類,用於定義用執行的任務。

public class DoSomethingTimerTask extends TimerTask {

    private String taskName;

    public DoSomethingTimerTask(String taskName) {
        this.taskName = taskName;
    }

    @Override
    public void run() {
        System.out.println(new Date() + " : 任務「" + taskName + "」被執行。");
    }
}

2.2.1指定延遲執行一次

 在指定延遲時間後執行一次,這類是比較常見的場景,

比如:當系統初始化某個組件之後,延遲幾秒中,然後進行定時任務的執行。

public class DelayOneDemo {

    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new DoSomethingTimerTask("DelayOneDemo"),1000L);
    }
}

執行上述代碼,延遲一秒之後執行定時任務,並打印結果。其中第二個參數單位為毫秒。

2.2.2固定間隔執行

在指定的延遲時間開始執行定時任務,定時任務按照固定的間隔進行執行。比如:延遲2秒執行,固定執行間隔為1秒。

public class PeriodDemo {

    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new DoSomethingTimerTask("PeriodDemo"),2000L,1000L);
    }
}

執行程序,會發現2秒之後開始每隔1秒執行一次。

2.2.3固定速率執行

在指定的延遲時間開始執行定時任務,定時任務按照固定的速率進行執行。

比如:延遲2秒執行,固定速率為1秒。

public class FixedRateDemo {

    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.scheduleAtFixedRate(new DoSomethingTimerTask("FixedRateDemo"),2000L,1000L);
    }
}

執行程序,會發現2秒之後開始每隔1秒執行一次。

此時,你是否疑惑schedulescheduleAtFixedRate效果一樣,為什麼提供兩個方法,它們有什麼區別?

2.3 schedule與scheduleAtFixedRate區別

在瞭解schedulescheduleAtFixedRate方法的區別之前,先看看它們的相同點:

  • 任務執行未超時,下次執行時間 = 上次執行開始時間 + period;
  • 任務執行超時,下次執行時間 = 上次執行結束時間;

在任務執行未超時時,它們都是上次執行時間加上間隔時間,來執行下一次任務。而執行超時時,都是立馬執行。

它們的不同點在於側重點不同,schedule方法側重保持間隔時間的穩定,而scheduleAtFixedRate方法更加側重於保持執行頻率的穩定。

2.3.1schedule側重保持間隔時間的穩定

schedule方法會因為前一個任務的延遲而導致其後面的定時任務延時。計算公式為scheduledExecutionTime(第n+1次) = realExecutionTime(第n次) + periodTime

也就是說如果第n次執行task時,由於某種原因這次執行時間過長,執行完後的systemCurrentTime>= scheduledExecutionTime(第n+1次),則此時不做時隔等待,立即執行第n+1次task。

而接下來的第n+2次task的scheduledExecutionTime(第n+2次)就隨著變成瞭realExecutionTime(第n+1次)+periodTime。這個方法更註重保持間隔時間的穩定。

2.3.2scheduleAtFixedRate保持執行頻率的穩定

scheduleAtFixedRate在反復執行一個task的計劃時,每一次執行這個task的計劃執行時間在最初就被定下來瞭,也就是scheduledExecutionTime(第n次)=firstExecuteTime +n*periodTime

如果第n次執行task時,由於某種原因這次執行時間過長,執行完後的systemCurrentTime>= scheduledExecutionTime(第n+1次),則此時不做period間隔等待,立即執行第n+1次task。

接下來的第n+2次的task的scheduledExecutionTime(第n+2次)依然還是firstExecuteTime+(n+2)*periodTime這在第一次執行task就定下來瞭。說白瞭,這個方法更註重保持執行頻率的穩定。

如果用一句話來描述任務執行超時之後schedule和scheduleAtFixedRate的區別就是:schedule的策略是錯過瞭就錯過瞭,後續按照新的節奏來走;scheduleAtFixedRate的策略是如果錯過瞭,就努力追上原來的節奏(制定好的節奏)。

2.4 Timer的缺陷

Timer計時器可以定時(指定時間執行任務)、延遲(延遲5秒執行任務)、周期性地執行任務(每隔個1秒執行任務)。但是,Timer存在一些缺陷。首先Timer對調度的支持是基於絕對時間的,而不是相對時間,所以它對系統時間的改變非常敏感。

其次Timer線程是不會捕獲異常的,如果TimerTask拋出的瞭未檢查異常則會導致Timer線程終止,同時Timer也不會重新恢復線程的執行,它會錯誤的認為整個Timer線程都會取消。同時,已經被安排單尚未執行的TimerTask也不會再執行瞭,新的任務也不能被調度。故如果TimerTask拋出未檢查的異常,Timer將會產生無法預料的行為。

3、JDK自帶ScheduledExecutorService

ScheduledExecutorService是JAVA 1.5後新增的定時任務接口,它是基於線程池設計的定時任務類,每個調度任務都會分配到線程池中的一個線程去執行。也就是說,任務是並發執行,互不影響。

需要註意:隻有當執行調度任務時,ScheduledExecutorService才會真正啟動一個線程,其餘時間ScheduledExecutorService都是出於輪詢任務的狀態。

ScheduledExecutorService主要有以下4個方法:

ScheduledFuture<?> schedule(Runnable command,long delay, TimeUnit unit);
<V> ScheduledFuture<V> schedule(Callable<V> callable,long delay, TimeUnit unit);
ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnitunit);
ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnitunit);

其中scheduleAtFixedRatescheduleWithFixedDelay在實現定時程序時比較方便,運用的也比較多。

ScheduledExecutorService中定義的這四個接口方法和Timer中對應的方法幾乎一樣,隻不過Timerscheduled方法需要在外部傳入一個TimerTask的抽象任務。 而ScheduledExecutorService封裝的更加細致瞭,傳RunnableCallable內部都會做一層封裝,封裝一個類似TimerTask的抽象任務類(ScheduledFutureTask)。然後傳入線程池,啟動線程去執行該任務。

3.1 scheduleAtFixedRate方法

scheduleAtFixedRate方法,按指定頻率周期執行某個任務。定義及參數說明:

public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
    long initialDelay,
    long period,
    TimeUnit unit);

參數對應含義:command為被執行的線程;initialDelay為初始化後延時執行時間;period為兩次開始執行最小間隔時間;unit為計時單位。

使用實例:

public class ScheduleAtFixedRateDemo implements Runnable{

    public static void main(String[] args) {
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
        executor.scheduleAtFixedRate(
                new ScheduleAtFixedRateDemo(),
                0,
                1000,
                TimeUnit.MILLISECONDS);
    }

    @Override
    public void run() {
        System.out.println(new Date() + " : 任務「ScheduleAtFixedRateDemo」被執行。");
        try {
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

上面是scheduleAtFixedRate方法的基本使用方式,但當執行程序時會發現它並不是間隔1秒執行的,而是間隔2秒執行。

這是因為,scheduleAtFixedRate是以period為間隔來執行任務的,如果任務執行時間小於period,則上次任務執行完成後會間隔period後再去執行下一次任務;但如果任務執行時間大於period,則上次任務執行完畢後會不間隔的立即開始下次任務。

3.2 scheduleWithFixedDelay方法

scheduleWithFixedDelay方法,按指定頻率間隔執行某個任務。定義及參數說明:

public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
    long initialDelay,
    long delay,
    TimeUnit unit);


參數對應含義:command為被執行的線程;initialDelay為初始化後延時執行時間;period為前一次執行結束到下一次執行開始的間隔時間(間隔執行延遲時間);unit為計時單位。

使用實例:

public class ScheduleAtFixedRateDemo implements Runnable{

    public static void main(String[] args) {
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
        executor.scheduleWithFixedDelay(
                new ScheduleAtFixedRateDemo(),
                0,
                1000,
                TimeUnit.MILLISECONDS);
    }

    @Override
    public void run() {
        System.out.println(new Date() + " : 任務「ScheduleAtFixedRateDemo」被執行。");
        try {
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

上面是scheduleWithFixedDelay方法的基本使用方式,但當執行程序時會發現它並不是間隔1秒執行的,而是間隔3秒。

這是因為scheduleWithFixedDelay是不管任務執行多久,都會等上一次任務執行完畢後再延遲delay後去執行下次任務。

4、Quartz框架實現

除瞭JDK自帶的API之外,我們還可以使用開源的框架來實現,比如Quartz

Quartz是Job scheduling(作業調度)領域的一個開源項目,Quartz既可以單獨使用也可以跟spring框架整合使用,在實際開發中一般會使用後者。使用Quartz可以開發一個或者多個定時任務,每個定時任務可以單獨指定執行的時間,例如每隔1小時執行一次、每個月第一天上午10點執行一次、每個月最後一天下午5點執行一次等。

Quartz通常有三部分組成:調度器(Scheduler)、任務(JobDetail)、觸發器(Trigger,包括SimpleTriggerCronTrigger)。下面以具體的實例進行說明。

4.1 Quartz集成

要使用Quartz,首先需要在項目的pom文件中引入相應的依賴:

<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>2.3.2</version>
</dependency>
<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz-jobs</artifactId>
    <version>2.3.2</version>
</dependency>

定義執行任務的Job,這裡要實現Quartz提供的Job接口:

public class PrintJob implements Job {
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println(new Date() + " : 任務「PrintJob」被執行。");
    }
}

創建SchedulerTrigger,並執行定時任務:

public class MyScheduler {

    public static void main(String[] args) throws SchedulerException {
        // 1、創建調度器Scheduler
        SchedulerFactory schedulerFactory = new StdSchedulerFactory();
        Scheduler scheduler = schedulerFactory.getScheduler();
        // 2、創建JobDetail實例,並與PrintJob類綁定(Job執行內容)
        JobDetail jobDetail = JobBuilder.newJob(PrintJob.class)
                .withIdentity("job", "group").build();
        // 3、構建Trigger實例,每隔1s執行一次
        Trigger trigger = TriggerBuilder.newTrigger().withIdentity("trigger", "triggerGroup")
                .startNow()//立即生效
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                        .withIntervalInSeconds(1)//每隔1s執行一次
                        .repeatForever()).build();//一直執行

        //4、Scheduler綁定Job和Trigger,並執行
        scheduler.scheduleJob(jobDetail, trigger);
        System.out.println("--------scheduler start ! ------------");
        scheduler.start();
    }
}

執行程序,可以看到每1秒執行一次定時任務。

在上述代碼中,其中JobQuartz的接口,業務邏輯的實現通過實現該接口來實現。

JobDetail綁定指定的Job,每次Scheduler調度執行一個Job的時候,首先會拿到對應的Job,然後創建該Job實例,再去執行Job中的execute()的內容,任務執行結束後,關聯的Job對象實例會被釋放,且會被JVM GC清除。

TriggerQuartz的觸發器,用於通知Scheduler何時去執行對應Job。SimpleTrigger可以實現在一個指定時間段內執行一次作業任務或一個時間段內多次執行作業任務。

CronTrigger功能非常強大,是基於日歷的作業調度,而SimpleTrigger是精準指定間隔,所以相比SimpleTriggerCroTrigger更加常用。CroTrigger是基於Cron表達式的。

常見的Cron表達式示例如下:

可以看出,基於QuartzCronTrigger可以實現非常豐富的定時任務場景。

5、Spring Task

Spring 3開始,Spring自帶瞭一套定時任務工具Spring-Task,可以把它看成是一個輕量級的Quartz,使用起來十分簡單,除Spring相關的包外不需要額外的包,支持註解和配置文件兩種形式。通常情況下在Spring體系內,針對簡單的定時任務,可直接使用Spring提供的功能。

基於XML配置文件的形式就不再介紹瞭,直接看基於註解形式的實現。

使用起來非常簡單,直接上代碼:

@Component("taskJob")
public class TaskJob {

    @Scheduled(cron = "0 0 3 * * ?")
    public void job1() {
        System.out.println("通過cron定義的定時任務");
    }

    @Scheduled(fixedDelay = 1000L)
    public void job2() {
        System.out.println("通過fixedDelay定義的定時任務");
    }

    @Scheduled(fixedRate = 1000L)
    public void job3() {
        System.out.println("通過fixedRate定義的定時任務");
    }
}

如果是在Spring Boot項目中,需要在啟動類上添加@EnableScheduling來開啟定時任務。

上述代碼中,@Component用於實例化類,這個與定時任務無關。@Scheduled指定該方法是基於定時任務進行執行,具體執行的頻次是由cron指定的表達式所決定。關於cron表達式上面CronTrigger所使用的表達式一致。與cron對照的,Spring還提供瞭fixedDelayfixedRate兩種形式的定時任務執行。

5.1 fixedDelay和fixedRate的區別

fixedDelayfixedRate的區別於Timer中的區別很相似。

fixedRate有一個時刻表的概念,在任務啟動時,T1、T2、T3就已經排好瞭執行的時刻,比如1分、2分、3分,當T1的執行時間大於1分鐘時,就會造成T2晚點,當T1執行完時T2立即執行。

fixedDelay比較簡單,表示上個任務結束,到下個任務開始的時間間隔。無論任務執行花費多少時間,兩個任務間的間隔始終是一致的。

5.2 Spring Task的缺點

Spring Task 本身不支持持久化,也沒有推出官方的分佈式集群模式,隻能靠開發者在業務應用中自己手動擴展實現,無法滿足可視化,易配置的需求。

6、分佈式任務調度

以上定時任務方案都是針對單機的,隻能在單個JVM進程中使用。而現在基本上都是分佈式場景,需要一套在分佈式環境下高性能、高可用、可擴展的分佈式任務調度框架。

6.1 Quartz分佈式

首先,Quartz是可以用於分佈式場景的,但需要基於數據庫鎖的形式。簡單來說,quartz的分佈式調度策略是以數據庫為邊界的一種異步策略。各個調度器都遵守一個基於數據庫鎖的操作規則從而保證瞭操作的唯一性,同時多個節點的異步運行保證瞭服務的可靠。

因此,Quartz的分佈式方案隻解決瞭任務高可用(減少單點故障)的問題,處理能力瓶頸在數據庫,而且沒有執行層面的任務分片,無法最大化效率,隻能依靠shedulex調度層面做分片,但是調度層做並行分片難以結合實際的運行資源情況做最優的分片。

6.2 輕量級神器XXL-Job

XXL-JOB是一個輕量級分佈式任務調度平臺。特點是平臺化,易部署,開發迅速、學習簡單、輕量級、易擴展。由調度中心和執行器功能完成定時任務的執行。調度中心負責統一調度,執行器負責接收調度並執行。

針對於中小型項目,此框架運用的比較多。

6.3 其他框架

除此之外,還有Elastic-JobSaturnSIA-TASK等。

Elastic-Job具有高可用的特性,是一個分佈式調度解決方案。

Saturn是唯品會開源的一個分佈式任務調度平臺,在Elastic Job的基礎上進行瞭改造。

SIA-TASK是宜信開源的分佈式任務調度平臺。

7、小結

通過本文梳理瞭6種定時任務的實現,就實踐場景的運用來說,目前大多數系統已經脫離瞭單機模式。對於並發量並不是太高的系統,xxl-job或許是一個不錯的選擇。

到此這篇關於Java中定時任務的6種實現方式的文章就介紹到這瞭,更多相關Java中定時任務實現方式內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: