Java中的定时任务 Timer ScheduledExecutorService Task(轻量级Quartz) Quartz

看了别人解释的各个定时任务,自己整理一下。

一、Timer 

Java自带的java.util.Timer类,该类允许你调度一个java.util.TimerTask任务。

可以理解Timer是一个调度类,TimerTask是某个具体的任务,被Timer调度。

在工具类Timer中,提供了四个构造方法,每个构造方法都启动了计时器线程,同时Timer类可以保证多个线程共享单个Timer对象而无需进行外部同步,所以Timer类是线程安全的。但是由于每一个Timer对象对应的是单个后台线程,用于顺序执行所有的计时器任务,一般情况下我们的线程任务执行所消耗的时间应该非常短,但是由于特殊情况导致某个定时器任务执行的时间太长,那么他就会“独占”计时器的任务执行线程,其后的所有线程都必须等待它执行完,这就会延迟后续任务的执行,使这些任务堆积在一起。

当程序初始化完成Timer后,定时任务就会按照我们设定的时间去执行,Timer提供了schedule方法,该方法有许多重载方式来适应不同的情况,如下:

schedule(TimerTask task, Date time):安排在指定的时间执行指定的任务。

schedule(TimerTask task, Date firstTime, long period) :安排指定的任务在指定的时间开始进行重复的固定延迟执行。

 schedule(TimerTask task, long delay) :安排在指定延迟后执行指定的任务。

schedule(TimerTask task, long delay, long period) :安排指定的任务从指定的延迟后开始进行重复的固定延迟执行。

同时也重载了scheduleAtFixedRate方法,scheduleAtFixedRate方法与schedule相同,只不过他们的侧重点不同。

scheduleAtFixedRate(TimerTask task, Date firstTime, long period):安排指定的任务在指定的时间开始进行重复的固定速率执行。

scheduleAtFixedRate(TimerTask task, long delay, long period):安排指定的任务在指定的延迟后开始进行重复的固定速率执行。

 

区别:

1.如果指定开始执行的时间在当前系统运行时间之前,scheduleAtFixedRate会把已经过去的时间也作为周期执行,而schedule不会把过去的时间算上。

比如,使用scheduleAtFixedRate方法,间隔时间是3分钟,指定开始时间是2018/09/12 14:10:00,如果在14:17:00分执行程序,则程序会执行3次(10、13、16),并且下一次执行是在14:19 而不是 14:20。也就是说从指定的开始时间计时,而不是从执行时间开始计时。

如果使用schedule方法,间隔时间是3分钟,指定开始时间是2018/09/12 14:10:00,那么在14:17:00分执行程序,则立即执行1次。并且下一次的执行时间是 14:20,而不是从14:10开始算的周期(14:19)。

 

2.在schedule方法中会因为前一个任务的延迟而导致其后面的定时任务延时,而scheduleAtFixedRate方法则不会,如果第n个task执行时间过长导致systemCurrentTime>= scheduledExecutionTime(n+1),则不会做任何等待,会立即执行第n+1个task。

举个例子,你有一个任务如果运行完需要8秒,而你设置30秒执行一次的话schedule和scheduleAtFixedRate的效果是一样的都是30秒执行一次。

如果你设置4秒执行一次,那么schedule执行第二次要等第一次执行完成,也就是说实际是8秒执行一次。scheduleAtFixedRate 是严格的4秒执行一次,第一次没有执行完成,第二次在4秒后也会执行。(注意这里的“执行”其实是另外开辟线程,因此之前的有没有运行完并不影响下一次的运行)

schedule方法侧重保存间隔时间的稳定,而scheduleAtFixedRate方法更加侧重于保持执行频率的稳定。

 

缺陷:

1.Timer对调度的支持是基于绝对时间的,而不是相对时间,所以它对系统时间的改变非常敏感。

2.Timer线程是不会捕获异常的,如果TimerTask抛出的了未检查异常则会导致Timer线程终止,同时Timer也不会重新恢复线程的执行,他会错误的认为整个Timer线程都会取消。同时,已经被安排但尚未执行的TimerTask也不会再执行了,新的任务也不能被调度。故如果TimerTask抛出未检查的异常,Timer将会产生无法预料的行为。

 对于Timer的缺陷,我们可以考虑 ScheduledThreadPoolExecutor 来替代。Timer是基于绝对时间的,对系统时间比较敏感,而ScheduledThreadPoolExecutor 则是基于相对时间。Timer是内部是单一线程,而ScheduledThreadPoolExecutor内部是个线程池,所以可以支持多个任务并发执行。

 

二、ScheduledExecutorService

java的线程池类ScheduledExecutorService也可以实现一些简单的定时任务,周期性任务。

ScheduledExecutorService是基于线程池设计的定时任务类,每个调度任务都会被分配到线程池中的一个线程去执行。也就是说,任务是并发执行的,即使有一个任务抛出异常停止也不会影响其它线程运行。

需要注意:只有当调度任务来的时候,ScheduledExecutorService才会真正启动一个线程,其余时间ScheduledExecutorService都是处于轮询任务的状态。

 

三、Task

Spring3.0以后自带的task,可以将它看成是一个轻量级的Quartz,而且使用起来比Quartz简单许多。支持注解和配置文件两种形式。

 

1、基于配置文件:

在spring配置文件的相应位置加入:

 

配置执行任务的类:

 

执行任务的类:

 

2、基于注解配置

定时任务开关

<task:annotation-driven />

注解扫描

<context:component-scan base-package="demo.*" />

任务类:

 

注解@Scheduled 中有三个方法,用来对执行规则的配置:

1)cron:指定cron表达式,将cron表达式配置在java的properties文件或者环境变量中,使配置更灵活。

<task:scheduled-tasks>

<task:scheduled ref=“taskTest” method="show” cron="${cron_expression}"/>

</task:scheduled-tasks>

 

@Scheduled(cron = "${cron_expression}")

 

2)fixedDelay:即表示从上一个任务完成开始到下一个任务开始的间隔,单位是毫秒。

3)fixedRate:即从上一个任务开始到下一个任务开始的间隔,单位是毫秒。

 

 

四、Quartz

Quartz是一个功能比较强大的的调度器,可以让你的程序在指定时间执行,也可以按照某一个频度执行,可以方便的分布式部署、便捷的监控和管理任务,适合任务很多的情况。Quartz默认使用RAMJobStore,它的优点是速度。因为所有的 Scheduler 信息都保存在计算机内存中,访问这些数据随着电脑而变快。而无须访问数据库或IO等操作,但它的缺点是将 Job 和 Trigger 信息存储在内存中的。因而我们每次重启程序,Scheduler 的状态,包括 Job 和 Trigger 信息都丢失了。Quartz 提供了两种类型的持久性 JobStore,为JobStoreTX和JobStoreCMT,其中: 

1. JobStoreTX为独立环境中的持久性存储,它设计为用于独立环境中。这里的 “独立”,我们是指这样一个环境,在其中不存在与应用容器的事物集成。这里并不意味着你不能在一个容器中使用 JobStoreTX,只不过,它不是设计来让它的事特受容器管理。区别就在于 Quartz 的事物是否要参与到容器的事物中去。 

2. JobStoreCMT 为程序容器中的持久性存储,它设计为当你想要程序容器来为你的 JobStore 管理事物时,并且那些事物要参与到容器管理的事物边界时使用。它的名字明显是来源于容器管理的事物(Container Managed Transactions (CMT))。

核心:

1、Job:是一个接口,只有一个方法void execute(JobExecutionContext context),开发者实现该接口定义运行任务,JobExecutionContext类提供了调度上下文的各种信息。Job运行时的信息保存在JobDataMap实例中;

2、JobDetail:Quartz在每次执行Job时,都重新创建一个Job实例,所以它不直接接受一个Job的实例,相反它接收一个Job实现类,以便运行时通过newInstance()的反射机制实例化Job。因此需要通过一个类来描述Job的实现类及其它相关的静态信息,如Job名字、描述、关联监听器等信息,JobDetail承担了这一角色。

3、Trigger:是一个类,描述触发Job执行的时间触发规则。主要有SimpleTrigger和CronTrigger这两个子类。当仅需触发一次或者以固定时间间隔周期执行,SimpleTrigger是最适合的选择;而CronTrigger则可以通过Cron表达式定义出各种复杂时间规则的调度方案。

4、Calendar:org.quartz.Calendar和java.util.Calendar不同,它是一些日历特定时间点的集合。一个Trigger可以和多个Calendar关联,以便排除或包含某些时间点。假设,我们安排每周星期一早上10:00执行任务,但是如果碰到法定的节日,任务则不执行,这时就需要在Trigger触发机制的基础上使用Calendar进行定点排除。

5、Scheduler:代表一个Quartz的独立运行容器,Trigger和JobDetail可以注册到Scheduler中,两者在Scheduler中拥有各自的组及名称,组及名称是Scheduler查找定位容器中某一对象的依据,Trigger的组及名称必须唯一,JobDetail的组和名称也必须唯一(但可以和Trigger的组和名称相同,因为它们是不同类型的)。Scheduler定义了多个接口方法,允许外部通过组及名称访问和控制容器中Trigger和JobDetail。

 6、 Scheduler可以将Trigger绑定到某一JobDetail中,这样当Trigger触发时,对应的Job就被执行。一个Job可以对应多个Trigger,但一个Trigger只能对应一个Job。可以通过SchedulerFactory创建一个Scheduler实例。Scheduler拥有一个SchedulerContext,它类似于ServletContext,保存着Scheduler上下文信息,Job和Trigger都可以访问SchedulerContext内的信息。SchedulerContext内部通过一个Map,以键值对的方式维护这些上下文数据,SchedulerContext为保存和获取数据提供了多个put()和getXxx()的方法。可以通过Scheduler的getContext()获取对应的SchedulerContext实例。

7、ThreadPool:Scheduler使用一个线程池作为任务运行的基础设施,任务通过共享线程池中的线程提高运行效率。

 

示例:

package org.quartz.examples; import org.quartz.*; import org.quartz.impl.StdSchedulerFactory; import java.util.ArrayList; import java.util.Date; import java.util.List; public class QuartzTest implements Job { /** * Quartz requires a public empty constructor so that the * scheduler can instantiate the class whenever it needs. */ public QuartzTest() { } /** * 该方法实现需要执行的任务 */ @SuppressWarnings("unchecked") @Override public void execute(JobExecutionContext context) throws JobExecutionException { // 从 context 中获取 instName, groupName 以及 dataMap String instName = context.getJobDetail().getKey().getName(); String groupName = context.getJobDetail().getKey().getGroup(); JobDataMap dataMap = context.getJobDetail().getJobDataMap(); // 从 dataMap 中获取 myDescription, myValue 以及 myArray String myDescription = dataMap.getString("myDescription"); int myValue = dataMap.getInt("myValue"); List<String> myArray = (List<String>) dataMap.get("myArray"); System.out.println("---> Instance = " + instName + ", group = " + groupName + ", description = " + myDescription + ", value =" + myValue + ", array item[0] = " + myArray.get(0)); System.out.println("Runtime: " + new Date().toString() + " <---"); } public static void main(String[] args) throws SchedulerException, InterruptedException { // 通过 schedulerFactory 获取一个调度器 SchedulerFactory sf = new StdSchedulerFactory(); Scheduler sched = sf.getScheduler(); // 创建 jobDetail 实例,绑定 Job 实现类 // 指明 job 的名称,所在组的名称,以及绑定 job 类 JobDetail job = JobBuilder.newJob(QuartzTest.class).withIdentity("job1", "group1").build(); // 定义调度触发规则 // SimpleTrigger,从当前时间的下 1 秒开始,每隔 1 秒执行 1 次,重复执行 2 次 /*Trigger trigger = TriggerBuilder.newTrigger() // 指明 trigger 的 name 和 group .withIdentity("trigger1", "group1") // 从当前时间的下 1 秒开始执行,默认为立即开始执行(.startNow()) .startAt(DateBuilder.evenSecondDate(new Date())) .withSchedule(SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds(1) // 每隔 1 秒执行 1 次 .withRepeatCount(2)) // 重复执行 2 次,一共执行 3 次 .build();*/ // corn 表达式,先立即执行一次,然后每隔 5 秒执行 1 次 Trigger trigger = TriggerBuilder.newTrigger() .withIdentity("trigger1", "group1") .withSchedule(CronScheduleBuilder.cronSchedule("*/5 * * * * ?")) .build(); // 初始化参数传递到 job job.getJobDataMap().put("myDescription", "Hello Quartz"); job.getJobDataMap().put("myValue", 1990); List<String> list = new ArrayList<>(); list.add("firstItem"); job.getJobDataMap().put("myArray", list); // 把作业和触发器注册到任务调度中 sched.scheduleJob(job, trigger); // 启动计划程序(实际上直到调度器已经启动才会开始运行) sched.start(); // 等待 10 秒,使我们的 job 有机会执行 Thread.sleep(10000); // 等待作业执行完成时才关闭调度器 sched.shutdown(true); } }

 

Quartz 的 Misfire (错失)处理规则

调度(scheduleJob)或恢复调度(resumeTrigger,resumeJob)后不同的misfire对应的处理规则

CronTrigger

withMisfireHandlingInstructionDoNothing

  • 不触发立即执行
  • 等待下次Cron触发频率到达时刻开始按照Cron频率依次执行

withMisfireHandlingInstructionIgnoreMisfires

  • 以错过的第一个频率时间立刻开始执行
  • 重做错过的所有频率周期后
  • 当下一次触发频率发生时间大于当前时间后,再按照正常的Cron频率依次执行

withMisfireHandlingInstructionFireAndProceed

  • 以当前时间为触发频率立刻触发一次执行
  • 然后按照Cron频率依次执行

SimpleTrigger

withMisfireHandlingInstructionFireNow

  • 以当前时间为触发频率立即触发执行
  • 执行至FinalTIme的剩余周期次数
  • 以调度或恢复调度的时刻为基准的周期频率,FinalTime根据剩余次数和当前时间计算得到
  • 调整后的FinalTime会略大于根据starttime计算的到的FinalTime值

withMisfireHandlingInstructionIgnoreMisfires

  • 以错过的第一个频率时间立刻开始执行
  • 重做错过的所有频率周期
  • 当下一次触发频率发生时间大于当前时间以后,按照Interval的依次执行剩下的频率
  • 共执行RepeatCount+1次

withMisfireHandlingInstructionNextWithExistingCount

  • 不触发立即执行
  • 等待下次触发频率周期时刻,执行至FinalTime的剩余周期次数
  • 以startTime为基准计算周期频率,并得到FinalTime
  • 即使中间出现pause,resume以后保持FinalTime时间不变

withMisfireHandlingInstructionNowWithExistingCount

  • 以当前时间为触发频率立即触发执行
  • 执行至FinalTIme的剩余周期次数
  • 以调度或恢复调度的时刻为基准的周期频率,FinalTime根据剩余次数和当前时间计算得到
  • 调整后的FinalTime会略大于根据starttime计算的到的FinalTime值

withMisfireHandlingInstructionNextWithRemainingCount

  • 不触发立即执行
  • 等待下次触发频率周期时刻,执行至FinalTime的剩余周期次数
  • 以startTime为基准计算周期频率,并得到FinalTime
  • 即使中间出现pause,resume以后保持FinalTime时间不变

withMisfireHandlingInstructionNowWithRemainingCount

  • 以当前时间为触发频率立即触发执行
  • 执行至FinalTIme的剩余周期次数
  • 以调度或恢复调度的时刻为基准的周期频率,FinalTime根据剩余次数和当前时间计算得到
  • 调整后的FinalTime会略大于根据starttime计算的到的FinalTime值

 

与spring整合:

spring提供了对quartz的整合,可以通过 org.springframework.scheduling.quartz.SchedulerFactoryBean 注入scheduler调度器,并且对调度器做些配置,比如使用线程池,并配置线程数量等。配置示例如下:

 

执行任务的类:

spring配置:

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值