相信大家都很熟悉java.util.Timer类,java类库中提供的简单的执行定时任务的类,使用也非常简单。自定义任务扩展抽象类TimeTask,实现抽象方法void run(),之后使用Timer对象的schedule( TimerTask task,long delay,long period )方法即可。
直观的观察此函数,意思是让任务延迟dealy 毫秒之后以period为周期执行。可是我们的需求一般是让某个任务整点或者一天的某个时间执行,那么该怎么做呢?
其实很简单,关键在于delay参数,我们可以借助 Calendar 间接实现该功能。比如某个任务需要整点执行。我们只需要设置delay大小为 程序启动时间所在小时的最后一秒减去当前时间。然后让任务以3600*1000的period执行。下面是计算当前时间未来的第一个整点。
Data day = new Date();
cal.setTime(day);
// cal.set(Calendar.HOUR_OF_DAY, cal.getMaximum(Calendar.HOUR_OF_DAY));
cal.set(Calendar.MINUTE,cal.getMaximum(Calendar.MINUTE));
cal.set(Calendar.SECOND,cal.getMaximum(Calendar.SECOND));
cal.set(Calendar.MILLISECOND,cal.getMaximum(Calendar.MILLISECOND));
return cal.getTime();
当然使用schedule(TimerTask task, Date firstTime, long period)更方便。
每天某个时间执行和每月,甚至每年也可以以此类推。
Timer 的设计核心是一个 TaskQueue 和一个 TaskThread。Timer 将接收到的任务丢到自己的 TaskQueue中,TaskQueue按照 Task 的最初执行时间进行排序。TimerThread 在创建 Timer 时会启动成为一个守护线程。这个线程会轮询所有任务,找到一个最近要执行的任务,然后休眠,当到达最近要执行任务的开始时间点,TimerThread 被唤醒并执行该任务。之后 TimerThread 更新最近一个要执行的任务,继续休眠。
某个定时任务整点统计上一小时内的某项数据,运行5天后发现,统计时间变成的xx:00:59秒。通过分析发现,因为这个统计很复杂,每次统计要花费大概0.5秒,这样一天下来就用12秒钟的消耗。这样执行的话,大概150天的时间,此任务执行共需要1800秒。这些统计的数据就会变为某个小时:30分到下一小时30分的统计数据..这显然偏离了程序的目的,为什么会这样呢?
问题在:schedule(TimerTask task, Date firstTime, long period)是用重复固定延迟触发的,每次执行之后每次执行时间为上一次任务结束起向后推一个时间间隔,即每次执行时间为:initialDelay, initialDelay+executeTime+period,initialDelay+2*executeTime+2* period。
解决办法是使用另外一个函数:scheduleAtFixedRate(TimerTask task, long delay,long period)
安排指定的任务在指定的延迟后开始进行重复的固定速率执行。
于是便引发出了触发器类型的概念。
一次性触发器(One-off)
重复固定延迟触发器(Fixed-Delay)
重复定时触发器(Fixed-Rated)
说明:
一次性触发器只能执行一次,执行完成后,不能被再度重新使用;
下面举例说明固定延迟触发器和定时触发器区别:
假如17:00开始任务执行,任务执行时间为30分钟,每小时执行一次,第一次运行将于17:30结束。如果采用固定延迟触发器,第二次运行将在 18:30开始,计算方法为前一次结束时间加上间隔时间;如果采用定时触发器,第二次运行将在18:00开始,计算方法为前一次开始时间加上间隔时间。
Timer 的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务。如果说某个任务出现了异常,这个定时器上的所有任务都会终止。
鉴于 Timer 的上述缺陷,Java 5 推出了基于线程池设计的 ScheduledExecutor。其设计思想是,每一个被调度的任务都会由线程池中一个线程去执行,因此任务是并发执行的,相互之间不会受到干扰。需要注意的是,只有当任务的执行时间到来时,ScheduedExecutor 才会真正启动一个线程,其余时间 ScheduledExecutor 都是在轮询任务的状态。
ScheduledExecutorService service = Executors.newScheduledThreadPool(10);
long initialDelay1 = 1;
long period1 = 1;
// 从现在开始1秒钟之后,每隔1秒钟执行一次job1
service.scheduleAtFixedRate(
new ScheduledExecutorTest("job1"), initialDelay1, period1, TimeUnit.SECONDS);
另外两种方式是使用开源类库Quartz 与 JCronTab,如何实现请参看:
具体实现见:https://www.ibm.com/developerworks/cn/java/j-lo-taskschedule/
Java常用的对任务进行调度的实现方法,即 Timer,ScheduledExecutor, Quartz 以及 JCronTab。对于简单的基于起始时间点与时间间隔的任务调度,使用 Timer 就足够了;如果需要同时调度多个任务,基于线程池的ScheduledTimer 是更为合适的选择;当任务调度的策略复杂到难以凭借起始时间点与时间间隔来描述时,Quartz与 JCronTab 则体现出它们的优势。熟悉Unix/Linux 的开发人员更倾向于 JCronTab,且JCronTab 更适合与 Web 应用服务器相结合。Quartz的 Trigger 与 Job 松耦合设计使其更适用于 Job 与 Trigger 的多对多应用场景。