上篇部落格《JAVA 併發程式設計-執行緒池(七)》中曾介紹到newScheduledThreadPool(intcorePoolSize),建立corePoolSize大小的執行緒池。此執行緒池支援定時以及週期性執行任務的需求。

    接下來我們一起來分析一下Java中幾種任務排程實現與比較

任務排程是指基於給定時間點,給定時間間隔或者給定執行次數自動執行任務。

Timer

相信大家都已經非常熟悉java.util.Timer 了,它是最簡單的一種實現任務排程的方法,下面給出一個具體的例子:

package com.tgb.hjy;

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

public class TimerTest extends TimerTask { 

	 private String jobName = ""; 

	 public TimerTest(String jobName) { 
		 super(); 
		 this.jobName = jobName; 
	 } 

	 @Override 
	 public void run() { 
		 System.out.println("execute " + jobName); 
	 } 

	 public static void main(String[] args) { 
		 Timer timer = new Timer(); 
		 long delay1 = 1 * 1000; 
		 long period1 = 1000; 
		 // 從現在開始 1 秒鐘之後,每隔 1 秒鐘執行一次 job1 
		 timer.schedule(new TimerTest("job1"), delay1, period1); 
		 long delay2 = 2 * 1000; 
		 long period2 = 2000; 
		 // 從現在開始 2 秒鐘之後,每隔 2 秒鐘執行一次 job2 
		 timer.schedule(new TimerTest("job2"), delay2, period2); 
	 	} 
	 } 

輸出結果:

execute job1

execute job1

execute job2

execute job1

execute job1

execute job2

execute job1

execute job1

execute job2

    使用 Timer 實現任務排程的核心類是Timer 和 TimerTask。其中 Timer 負責設定 TimerTask 的起始與間隔執行時間。使用者只需要建立一個 TimerTask的繼承類,實現自己的 run 方法,然後將其丟給 Timer 去執行即可。

    Timer 的設計核心是一個 TaskList和一個 TaskThread。Timer 將接收到的任務丟到自己的 TaskList 中,TaskList 按照 Task的最初執行時間進行排序。TimerThread 在建立 Timer時會啟動成為一個守護執行緒。這個執行緒會輪詢所有任務,找到一個最近要執行的任務,然後休眠,當到達最近要執行任務的開始時間點,TimerThread被喚醒並執行該任務。之後 TimerThread 更新最近一個要執行的任務,繼續休眠。

    Timer的優點在於簡單易用,但由於所有任務都是由同一個執行緒來排程,因此所有任務都是序列執行的,同一時間只能有一個任務在執行,前一個任務的延遲或異常都將會影響到之後的任務。

ScheduledExecutor

    鑑於 Timer 的上述缺陷,Java 5推出了基於執行緒池設計的ScheduledExecutor。其設計思想是,每一個被排程的任務都會由執行緒池中一個執行緒去執行,因此任務是併發執行的,相互之間不會受到干擾。需要注意的是,只有當任務的執行時間到來時,ScheduedExecutor才會真正啟動一個執行緒,其餘時間 ScheduledExecutor 都是在輪詢任務的狀態。

package com.tgb.hjy;

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

public class ScheduledExecutorTest implements Runnable {
	private String jobName = "";

	public ScheduledExecutorTest(String jobName) {
		super();
		this.jobName = jobName;
	}

	@Override
	public void run() {
		System.out.println("execute " + jobName);
	}

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

		long initialDelay1 = 1;
		long period1 = 1;
        // 從現在開始1秒鐘之後,每隔1秒鐘執行一次job1 scheduleAtFixedRate
		//每次執行時間為上一次任務開始起向後推一個時間間隔
		//已固定的頻率來執行某項計劃(任務).固定的頻率來執行某項計劃,它不受計劃執行時間的影響。到時間,它就執行。
		service.scheduleAtFixedRate(
		        new ScheduledExecutorTest("job1"), initialDelay1,
				period1, TimeUnit.SECONDS);

		long initialDelay2 = 1;
		long delay2 = 1;
        // 從現在開始2秒鐘之後,每隔2秒鐘執行一次job2 scheduleWithFixedDelay
		//每次執行時間為上一次任務結束起向後推一個時間間隔
		//相對固定的延遲後,執行某項計劃.即無論某個任務執行多長時間,等執行完了,我再延遲指定的時間。它受計劃執行時間的影響。
		service.scheduleWithFixedDelay(
		        new ScheduledExecutorTest("job2"), initialDelay2,
				delay2, TimeUnit.SECONDS);
	}
}

執行結果:

execute job1

execute job2

execute job1

execute job2

execute job1

execute job2

    ScheduledExecutorService中兩種最常用的排程方法 ScheduleAtFixedRate 和 ScheduleWithFixedDelay。ScheduleAtFixedRate每次執行時間為上一次任務開始起向後推一個時間間隔;ScheduleWithFixedDelay每次執行時間為上一次任務結束起向後推一個時間間隔。由此可見,ScheduleAtFixedRate是基於固定時間間隔進行任務排程,ScheduleWithFixedDelay 取決於每次任務執行的時間長短,是基於不固定時間間隔進行任務排程。

用 ScheduledExecutor 和Calendar 實現複雜任務排程

    Timer 和 ScheduledExecutor都僅能提供基於開始時間與重複間隔的任務排程,不能勝任更加複雜的排程需求。比如,設定每星期二的 16:38:10 執行任務。該功能使用 Timer 和ScheduledExecutor 都不能直接實現,但我們可以藉助 Calendar 間接實現該功能。

    注,上述方法實現該任務排程比較麻煩,這就需要一個更加完善的任務排程框架來解決這些複雜的排程問題。幸運的是,開源工具包Quartz提供了這方面強大的支援。

Quartz

Quartz可以滿足更多更復雜的排程需求,首先讓我們看看如何用 Quartz 實現每星期二 16:38 的排程安排:

package com.tgb.hjy;

import java.util.Date;

import org.quartz.Job;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.Scheduler;
import org.quartz.SchedulerFactory;
import org.quartz.Trigger;
import org.quartz.helpers.TriggerUtils;

public class QuartzTest implements Job {

	@Override
	//該方法實現需要執行的任務
	public void execute(JobExecutionContext arg0) throws JobExecutionException {
		System.out.println("Generating report - "
				+ arg0.getJobDetail().getFullName() + ", type ="
				+ arg0.getJobDetail().getJobDataMap().get("type"));
		System.out.println(new Date().toString());
	}
	public static void main(String[] args) {
		try {
			// 建立一個Scheduler
			SchedulerFactory schedFact = 
			new org.quartz.impl.StdSchedulerFactory();
			Scheduler sched = schedFact.getScheduler();
			sched.start();
			// 建立一個JobDetail,指明name,groupname,以及具體的Job類名,
			//該Job負責定義需要執行任務
			JobDetail jobDetail = new JobDetail("myJob", "myJobGroup",
					QuartzTest.class);
			jobDetail.getJobDataMap().put("type", "FULL");
            // 建立一個每週觸發的Trigger,指明星期幾幾點幾分執行
			Trigger trigger = TriggerUtils.makeWeeklyTrigger(3, 16, 38);
			trigger.setGroup("myTriggerGroup");
			// 從當前時間的下一秒開始執行
			trigger.setStartTime(TriggerUtils.getEvenSecondDate(new Date()));
			// 指明trigger的name
			trigger.setName("myTrigger");
			// 用scheduler將JobDetail與Trigger關聯在一起,開始排程任務
			sched.scheduleJob(jobDetail, trigger);

		} catch (Exception e) {
			e.printStackTrace();
		}
	}
} 

    使用Quartz我們非常簡潔地實現了一個上述複雜的任務排程。Quartz設計的核心類包括 Scheduler, Job 以及 Trigger。其中,Job 負責定義需要執行的任務,Trigger負責設定排程策略,Scheduler 將二者組裝在一起,並觸發任務開始執行。

總結:

以上是幾種任務排程的實現方式,各有優缺點,對於Quartz的學習還需要繼續深入。後面會詳細介紹!