几种任务调度的Java实现方案对比【咕泡学院】


简介

目前大多数企业都会用到定时调度功能,比如用来做日志归集、 定时做对账、文件处理解析等等。。。

任务调度是指基于给定时间点,给定时间间隔或者给定执行次数自动执行任务,大家如果对我们的内容感兴趣的话,QQ群搜索788692365,分享更多视频教程和技术资料

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 和一个 TimerThread。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 每次执行时间为上一次任务结束起向后推一个时间间隔

源码阅读:http://ju.outofmemory.cn/entry/99456

线程池四种中断策略

AbortPolicy(中止):它是默认的策略。

CallerRunsPolicy (调用者运行):它既不会丢弃任务,也不会抛出任何异常,它会把任务推回到调用者那里去,以此缓解任务流

DiscardPolicy(遗弃)策略:它默认会放弃这个任务

DiscardOldestPolicy(遗弃最旧的):它选择的丢弃的任务,是它本来要执行的(可怜的娃,就这样被新加入的给排挤了)

复杂案例

如果我们要实现一个每周二晚上21点10分执行的任务,那我们用ScheduleExecutorService怎么实现呢?

第一步


第二步

这个时候会有一个问题,如果我要执行的时间是周一呢? WEEKOfYear

优缺点

我们没办法很简单的通过ScheduledExecutor来实现一个周期定时调度,比如我需要每天下午3点去执行一个任务

Quartz

Quartz 可以满足更多更复杂的调度需求

引入jar包

<dependency>

代码演示


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 则体现出它们的优势

免费学习资料和视频可以QQ搜索 788692365 Java架构群,即可获得(JVM、多线程、分布式、高并发、高可用、nginx、redis、mysql、zookeeper、dubbo等),


 


-- --
  • 投诉或建议
评论