简介
目前大多数企业都会用到定时调度功能,比如用来做日志归集、 定时做对账、文件处理解析等等。。。
Timer
相信大家都已经非常熟悉 Java.util.Timer 了,它是最简单的一种实现任务调度的方法
使用 Timer 实现任务调度的核心类是 Timer 和 TimerTask。其中 Timer 负责设定 TimerTask 的起始与间隔执行时间。使用者只需要创建一个 TimerTask 的继承类,实现自己的 run 方法,然后将其丢给 Timer 去执行即可。
演示代码
timer.schedule 每次执行时间为上一次任务结束起向后推一个时间间隔; 取决于每次任务执行的时间长短,是基于不固定时间间隔进行任务调度
timer.scheduleAtFixedRate 每次执行时间为上一次任务开始起向后推一个时间间隔; 是基于固定时间间隔进行任务调度
当执行任务的时间大于周期间隔时,会发生什么呢?
(1)schedule方法:下一次执行时间相对于 上一次 实际执行完成的时间点 ,因此执行时间会不断延后
(2)scheduleAtFixedRate方法:下一次执行时间相对于上一次开始的 时间点 ,因此执行时间不会延后,存在并发性
我们通过代码来证明一下这两个方法的效果,我们假设timertask需要执行6秒钟,我们把时间间隔周期设置为5秒,分别看看这两个方法的执行结果
结论
schedule:下一次的执行时间点=上一次程序执行完成的时间点+间隔时间
scheduleAtFixedRate:下一次的执行时间点=上一次程序开始执行的时间点+间隔时间
原理
Timer 的设计核心是一个 TaskQueue 和一个 TaskThread。Timer 将接收到的任务丢到自己的 TaskQueue中。TimerThread 在创建 Timer 时会启动成为一个守护线程。这个线程会轮询所有任务,找到一个最近要执行的任务,然后休眠,当到达最近要执行任务的开始时间点,TimerThread 被唤醒并执行该任务。之后 TimerThread 更新最近一个要执行的任务,继续休眠。
优缺点
Timer 的优点在于简单易用;
但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行
前一个任务的延迟或异常都将会影响到之后的任务。
ScheduledExecutor
鉴于 Timer 的上述缺陷,Java 5 推出了基于线程池设计的 ScheduledExecutor。其设计思想是,每一个被调度的任务都会由线程池中一个线程去执行,因此任务是并发执行的,相互之间不会受到干扰。需要注意的是,只有当任务的执行时间到来时,ScheduedExecutor 才会真正启动一个线程,其余时间 ScheduledExecutor 都是在轮询任务的状态。
newFixedThreadPool
创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中
newCachedThreadPool
创建一个可缓存的线程池。这种类型的线程池特点是:
1).工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
2).如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程
newSingleThreadExecutor
创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的 。
newScheduleThreadPool
创建一个定长的线程池,而且支持定时的以及周期性的任务执行,类似于Timer
FixedThreadPool
是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。
CachedThreadPool
特点是在线程池空闲时,即线程池中没有可运行任务时,它会释放工作线程,从而释放工作线程所占用的资源。但是,但当出现新任务时,又要创建一新的工作线程,又要一定的系统开销。并且,在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪
代码演示
原理
展示了 ScheduledExecutorService 中两种最常用的调度方法 ScheduleAtFixedRate 和 ScheduleWithFixedDelay。
ScheduleAtFixedRate 每次执行时间为上一次任务开始起向后推一个时间间隔
ScheduleWithFixedDelay 每次执行时间为上一次任务结束起向后推一个时间间隔
线程池四种中断策略
AbortPolicy(中止):它是默认的策略。
CallerRunsPolicy (调用者运行):它既不会丢弃任务,也不会抛出任何异常,它会把任务推回到调用者那里去,以此缓解任务流
DiscardPolicy(遗弃)策略:它默认会放弃这个任务
DiscardOldestPolicy(遗弃最旧的):它选择的丢弃的任务,是它本来要执行的(可怜的娃,就这样被新加入的给排挤了)
复杂案例
如果我们要实现一个每周二晚上21点10分执行的任务,那我们用ScheduleExecutorService怎么实现呢?
第一步
第二步
这个时候会有一个问题,如果我要执行的时间是周一呢? WEEKOfYear
优缺点
我们没办法很简单的通过ScheduledExecutor来实现一个周期定时调度,比如我需要每天下午3点去执行一个任务
Quartz
Quartz 可以满足更多更复杂的调度需求
引入jar包
代码演示
Job
使用者只需要创建一个 Job 的继承类,实现 execute 方法。JobDetail 负责封装 Job 以及 Job 的属性,并将其提供给 Scheduler 作为参数。每次 Scheduler 执行任务时,首先会创建一个 Job 的实例,然后再调用 execute 方法执行。Quartz 没有为 Job 设计带参数的构造函数,因此需要通过额外的 JobDataMap 来存储 Job 的属性。JobDataMap 可以存储任意数量的 Key,Value 对
jobDetail.getJobDataMap().put("myDescription", "my job description");
Trigger
Trigger 的作用是设置调度策略。Quartz 设计了多种类型的 Trigger,其中最常用的是 SimpleTrigger 和 CronTrigger。
SimpleTrigger
适用于在某一特定的时间执行一次,或者在某一特定的时间以某一特定时间间隔执行多次,具体功能决定了 SimpleTrigger 的参数包括 start-time, end-time, repeat count, 以及 repeat interval。
public SimpleTrigger(String name, String group, Date startTime, Date endTime, int repeatCount, long repeatInterval)
创建一个立即执行且仅执行一次的 SimpleTrigger
SimpleTrigger trigger=new SimpleTrigger(“myTrigger”, “myGroup”, new Date(), null, 0, 0L);
CronTrigger
CronTrigger 的用途更广,相比基于特定时间间隔进行调度安排的 SimpleTrigger,CronTrigger 主要适用于基于日历的调度安排。例如:每星期二的 16:38:10 执行,每月一号执行,以及更复杂的调度安排等
CronTrigger的核心在于Cron表达式,该表达式由七个字段组成
Seconds:可出现”, - * /”四个字符,有效范围为0-59的整数
Minutes:可出现”, - * /”四个字符,有效范围为0-59的整数
Hours:可出现”, - * /”四个字符,有效范围为0-23的整数
DayofMonth:可出现”, - * / ? L W C”八个字符,有效范围为0-31的整数 (问号表示用来指明没有特定的值,只可能出现在DayOfMonth和DayOfWeek上)
Month:可出现”, - * /”四个字符,有效范围为1-12的整数或JAN-DEc
DayofWeek:可出现”, - * / ? L C #”四个字符,有效范围为1-7的整数或SUN-SAT两个范围。1表示星期天,2表示星期一, 依次类推
Year:可出现”, - * /”四个字符,有效范围为1970-2099年
(1):表示匹配该域的任意值,假如在Minutes域使用, 即表示每分钟都会触发事件。
(2)-:表示范围,例如在Minutes域使用5-20,表示从5分到20分钟每分钟触发一次
(3)/:表示起始时间开始触发,然后每隔固定时间触发一次,例如在Minutes域使用5/20,则意味着5分钟触发一次,而25,45等分别触发一次.
(4) ,:表示列出枚举值值。例如:在Minutes域使用5,20,则意味着在5和20分每分钟触发一次。
(5) L:表示最后,只能出现在DayofWeek和DayofMonth域,如果在DayofWeek域使用5L,意味着在最后的一个星期四触发。
(6) W:表示有效工作日(周一到周五),只能出现在DayofMonth域, 系统将在离指定日期的最近的有效工作日触发事件。例如:在 DayofMonth使用5W,如果5日是星期六,则将在最近的工作日:星期五,即4日触发。如果5日是星期天,则在6日(周一)触发;如果5日在星期一到星期五中的一天,则就在5日触发。另外一点,W的最近寻找不会跨过月份
(7)#:用于确定每个月第几个星期几,例如 Day-of-Week 赋值为 5#2 或者 THU#2,表示该月第二个星期四
Listener
除了上述基本的调度功能,Quartz 还提供了 listener 的功能。主要包含三种 listener:JobListener,TriggerListener 以及 SchedulerListener。当系统发生故障,相关人员需要被通知时,Listener 便能发挥它的作用。最常见的情况是,当任务被执行时,系统发生故障,Listener 监听到错误,立即发送邮件给管理员
JobListener 对执行的任务建立监听
TriggerListener 对调度进行监听
SchedulerListener 监听scheduler自身的消息,job/trigger的增加、job/trigger的删除、scheduler内部发生的严重错误以及scheduler关闭的消息等;
代码演示
1. 添加mylistenner, 实现JobListenner接口
异常测试
为了测试 listener 的功能,可以在 job 的 execute 方法中强制抛出异常。listener 接收到异常,将 job 所在的 scheduler 停掉,阻止后续的 job 继续执行。scheduler、jobDetail 等信息都可以从 listener 的参数 context 中检索到。
JcronTab
习惯使用 unix/Linux 的开发人员应该对 crontab 都不陌生。Crontab 是一个非常方便的用于 unix/linux 系统的任务调度命令。JCronTab 则是一款完全按照 crontab 语法编写的 java 任务调度工具
首先简单介绍一下 crontab 的语法,与上面介绍的 Quartz 非常相似,但更加简洁 , 集中了最常用的语法。主要由六个字段组成(括弧中标识了每个字段的取值范围):
Minutes (0-59)
Hours (0-23)
Day-of-Month (1-31)
Month (1-12/JAN-DEC)
Day-of-Week (0-6/SUN-SAT)
Command
总结
我们对常用的每种方法都进行了实例解释,并对其优缺点进行比较。对于简单的基于起始时间点与时间间隔的任务调度,使用 Timer 就足够了;如果需要同时调度多个任务,基于线程池的 ScheduledTimer 是更为合适的选择;当任务调度的策略复杂到难以凭借起始时间点与时间间隔来描述时,Quartz 与 JCronTab 则体现出它们的优势