0
点赞
收藏
分享

微信扫一扫

Java基础之定时任务调度


概述

任务调度是指基于给定时间点,给定时间间隔或者给定执行次数自动执行任务。在Java里面的有很多工具可供使用:

  1. Timer:用得很少
  2. ScheduledExecutorService
  3. Spring SchedulingTaskExecutor
  4. Quartz

实际业务中,任务调度,又有单机调度和分布式集群多节点调度。本文只单机调度,分布式任务调度,参考​​分布式任务调度系统​​。

Timer

使用 Timer 实现任务调度的核心类是 Timer 和 TimerTask。Timer 负责设定 TimerTask 的起始与间隔执行时间。使用者只需要创建一个 TimerTask 的继承类,实现自己的 run 方法,然后将其丢给 Timer 去执行即可。TimerTask是一个抽象类,它实现Runnable接口,因此可以用它来实现多线程。TimerTask一般是以匿名类的方式创建,当然也可以创建一个继承于TimerTask的类。

Timer 的设计核心是TaskList 和TaskThread。Timer 将接收到的任务丢到自己的 TaskList 中,TaskList 按照 Task 的最初执行时间进行排序。TimerThread 在创建 Timer 时会启动成为一个守护线程。这个线程会轮询所有任务,找到一个最近要执行的任务,然后休眠,当到达最近要执行任务的开始时间点,TimerThread 被唤醒并执行该任务。之后 TimerThread 更新最近一个要执行的任务,继续休眠。

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

入门

public class TimerDemo {
public static void main(String[] args) {
Timer timer = new Timer();
// 构造函数new Timer(true)
// 表明这个timer以daemon方式运行(优先级低,程序结束timer也自动结束)
TimerTask task = new TimerTask() {
public void run() {
System.out.println("sending messages...");
}
};
Date time = new Date();
long delay = 3000;
long period = 5000;
// Timer提供4个重载的schedule方法
// time为Date类型:在指定时间执行一次
timer.schedule(task, time);

// 安排指定的任务在指定的时间开始进行重复的固定延迟执行
//timer.schedule(task, time, period);

// 从现在起过delay毫秒执行一次
//timer.schedule(task, delay);

// 从现在起过delay毫秒以后,每隔period毫秒执行一次
//timer.schedule(task, 3000, 5000);
}
}

如果在创建Timer的实例时使用的是​​timer(true)​​​这个构造方法,那么只有​​timer.schedule(task, time)​​​和​​timer.schedule(task,time, period)​​​会成功执行,因为这个构造方法表示是以守护进程的方式运行,守护进程之后在程序还在运行的时候才会运行,当执行一次后,除了这个守护进程没有其他代码需要执行,所以这个守护进程就没有存在的意义,所以只有一次执行。而其他两个重载方法,因为从始至终只有守护进程存在,所以守护进程不会运行,也就不会执行run方法。
Timer定时器还有两个方法:

// 让任务在time时间执行一次,然后每隔period时间间隔执行一次
timer.scheduleAtFixedRate(task, time, period);
// 让任务在延迟delay毫秒后执行,然后每隔period时间间隔执行一次
timer.scheduleAtFixedRate(task, delay, period);

这两个方法都表示以固定的频率执行某一任务,区别在于:
如果任务是1秒钟后执行,然后每隔3秒执行一次,但是当资源调度紧张时,示例中的代码可能会在1秒钟执行后,导致4秒钟后才执行下一次;
但是scheduleAtFixedRate方法,如果因为任务繁忙,1秒钟后执行一次任务后,3.5秒后才开始执行下一次任务,此时java会记下这个延迟0.5秒,会让下载任务在2.5秒后就执行。

定时器的终止:默认情况下,如果一个程序的timer还在执行,那么这个程序就会一直在运行。
终止一个定时器主要有一下三种方法:

  1. 调用​​timer.cancel()​​方法,可以在程序的任何地方调用此方法,甚至可以在TimerTask的run方法里使用此方法;
  2. 让timer定时器成为一个守护进程,这样当程序只有守护进程存在时,守护进程就会停止运行,程序自然也会停止,而让timer定时器成为一个守护进程的方法是使用Timer的timer(true)构造方法;
  3. 调用​​System.exit(int arg0)​​方法,这样程序停止,timer自然停止。

当程序的timer在运行时,程序就会保持运行,但是当timer中的所有TimerTask运行完了,整个程序会结束吗,答案是否定的,比如​​timer.shedule(task,5000)​​,5秒之后,其实整个程序还没有退出,timer会等待垃圾回收后,然后程序才会得以退出,具体的参照http://www.douban.com/note/64661564/;所以在TimerTask的run函数执行完毕之后加上System.gc()就可以。

schedule和scheduleAtFixedRate

schedule(TimerTask task, Date time);
schedule(TimerTask task, long delay);

这两个方法,如果指定的计划执行时间scheduledExecutionTime<= systemCurrentTime,则task会被立即执行。scheduledExecutionTime不会因为某一个task的过度执行而改变。

schedule(TimerTask task, Date firstTime, long period);
schedule(TimerTask task, long delay, long period);

Timer的计时器任务会因为前一个任务执行时间较长而延时。在这两个方法中,每一次执行的task的计划时间会随着前一个task的实际时间而发生改变,也就是​​scheduledExecutionTime(n+1)=realExecutionTime(n)+periodTime​​​。也就是说如果第n个task由于某种情况导致这次的执行时间过程,最后导致​​systemCurrentTime>= scheduledExecutionTime(n+1)​​​,这时第n+1个task并不会因为到时而执行,他会等待第n个task执行完之后再执行,那么这样势必会导致n+2个的执行实现scheduledExecutionTime放生改变即​​scheduledExecutionTime(n+2) = realExecutionTime(n+1)+periodTime​​。所以这两个方法更加注重保存间隔时间的稳定。

scheduleAtFixedRate(TimerTask task, Date firstTime, long period);
scheduleAtFixedRate(TimerTask task, long delay, long period);

scheduleAtFixedRate与schedule方法的侧重点不同,schedule方法侧重保持间隔时间的稳定,而scheduleAtFixedRate方法更加侧重于保持执行频率的稳定。在schedule方法中会因为前一个任务的延迟而导致其后面的定时任务延时,而scheduleAtFixedRate方法则不会,如果第n个task执行时间过长导致​​systemCurrentTime>= scheduledExecutionTime(n+1)​​,则不会做任何等待他会立即执行第n+1个task,所以scheduleAtFixedRate方法执行时间的计算方法不同于schedule,而是scheduledExecutionTime(n)=firstExecuteTime +n*periodTime,该计算方法永远保持不变。

缺点

由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务。
多线程并行处理定时任务时,Timer 运行多个 TimerTask 时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行。

Timer计时器可以定时(指定时间执行任务)、延迟(延迟5秒执行任务)、周期性地执行任务(每隔个1秒执行任务)。
但是,Timer存在一些缺陷:

  1. Timer对调度的支持是基于绝对时间的,而不是相对时间,所以它对系统时间的改变非常敏感。Timer在执行定时任务时只会创建一个线程任务,如果存在多个线程,若其中某个线程因为某种原因而导致线程任务执行时间过长,超过两个任务的间隔时间,会发生一些缺陷
  2. Timer线程是不会捕获异常的,如果TimerTask抛出未检查异常则会导致Timer线程终止,同时Timer也不会重新恢复线程的执行,他会错误的为整个Timer线程都会取消。同时,已经被安排单尚未执行的TimerTask也不会再执行,新的任务也不能被调度。故如果TimerTask抛出未检查的异常,Timer将会产生无法预料的行为。

对于Timer的缺陷,可以考虑 ScheduledThreadPoolExecutor(STPE) 来替代。STPE 基于相对时间;Timer内部是单一线程,而STPE内部是个线程池,所以可以支持多个任务并发执行。

ScheduledExecutorService

JDK1.5 之后推荐使用ScheduledThreadPoolExecutor(STPE,是ScheduledExecutorService接口的实现类,简称SES)。STPE继承自ThreadPoolExecutor,本质上来说STPE是一个线程池,它也有 coorPoolSize和workQueue,也接受 Runnable 的子类作为任务,与一般线程池不一样的地方在于它实现自己的工作队列 DelayedWorkQueue,这个队列会按照一定顺序对队列中的任务进行排序。

设计思想:每一个被调度的任务都会由线程池中一个线程去执行,任务并发执行,相互之间不会受到干扰。只有当任务的执行时间到来时,SES才会真正启动一个线程,其余时间SES都是在轮询任务的状态。

设计这个API用于解决Timer的缺陷:

  1. Timer 对提交的任务调度是基于绝对时间而不是相对时间,通过其提交的任务对系统时钟的改变是敏感的(譬如提交延迟任务后修改系统时间会影响其执行);而SES只支持相对时间,对系统时间不敏感。
  2. 因为 Timer 线程并不捕获异常,所以 TimerTask 抛出的未检查异常会使 Timer 线程终止,后续提交的任务得不到执行,将会产生无法预料的行为;而STPE不存在此问题。

类图:

Java基础之定时任务调度_执行时间


接口源码:

public interface ScheduledExecutorService extends ExecutorService {
ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);
<V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit);
ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit);
ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit);

前2个方法是在一段时间后(delay,unit决定),开启一个任务,任务只运行一次。后2个方法是每隔一段时间定时触发一个任务,其中方法3是每隔固定的时间启动一个任务(若前一个任务还未完成,则下一个任务可能会一点,不会并发的运行);方法4是前一个任务运行完成后经过指定的时间运行下一个任务。

SES中两种最常用的调度方法 scheduleAtFixedRate和scheduleWithFixedDelay。前者每次执行时间为上一次任务开始起向后推一个时间间隔 :​​initialDelay, initialDelay+period, initialDelay+2*period, …​​​;后者每次执行时间为上一次任务结束起向后推一个时间间隔:​​initialDelay, initialDelay+executeTime+delay, initialDelay+2*executeTime+2*delay​​。前者是基于固定时间间隔进行任务调度,后者取决于每次任务执行的时间长短,是基于不固定时间间隔进行任务调度。

一般直接使用实现类ScheduledThreadPoolExecutor,其schedule方法:

public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
RunnableScheduledFuture<?> t = decorateTask(command, new ScheduledFutureTask<Void>(command, null, triggerTime(delay, unit)));
delayedExecute(t);
return t;
}

scheduleAtFixedRate方法:

public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
if (period <= 0)
throw new IllegalArgumentException();
// 接口方法3和4的区别所在
ScheduledFutureTask<Void> sft = new ScheduledFutureTask<>(command, null, triggerTime(initialDelay, unit), unit.toNanos(period));
RunnableScheduledFuture<Void> t = decorateTask(command, sft);
sft.outerTask = t;
delayedExecute(t);
return t;
}

schedule和scheduleAtFixedRate方法的差别仅在​​sft.outerTask = t​​​。
第2个方法和第1个方法的区别,仅仅在于前者可以返回结果。
第4个方法和第3个方法的区别,仅仅在于前者构造ScheduledFutureTask的入参:

ScheduledFutureTask<Void> sft = new ScheduledFutureTask<>(command, null, triggerTime(initialDelay, unit), unit.toNanos(-delay));

Spring Task

Spring Task底层是基于 JDK 的 ScheduledThreadPoolExecutor 线程池来实现的。入门实例:

@Slf4j
@Component
public class ScheduledService {
@Scheduled(cron = "0/5 * * * * *")
public void scheduled(){
}

@Scheduled(fixedRate = 5000)
public void scheduled1() {
}

@Scheduled(fixedDelay = 5000)
public void scheduled2() {
}

}

在Main启动类上使用@EnableScheduling注解开启对定时任务的支持即可。同一个线程中串行执行,如果只有一个定时任务,这样做肯定没问题,当定时任务增多,如果一个任务卡死,会导致其他任务也无法执行。

多线程执行
配置类:

@Configuration
@EnableAsync
public class AsyncConfig {
private int corePoolSize = 10;
private int maxPoolSize = 200;
private int queueCapacity = 10;

@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setQueueCapacity(queueCapacity);
executor.initialize();
return executor;
}
}

在定时任务的类或者方法上添加@Async,

Quartz

SB版本是2.0.0+,则​​spring-boot-starter-quartz​​中已经包含quart的依赖,可直接使用:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

如果是1.5.9则要使用以下添加依赖:

<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>

创建demo类TestQuartz继承QuartzJobBean:

public class TestQuartz extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
log.info("quartz task " + new Date());
}
}

配置类QuartzConfig

@Configuration
public class QuartzConfig {

@Bean
public JobDetail teatQuartzDetail(){
return JobBuilder.newJob(TestQuartz.class).withIdentity("testQuartz").storeDurably().build();
}

@Bean
public Trigger testQuartzTrigger(){
SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(10)
.repeatForever();
return TriggerBuilder.newTrigger().forJob(teatQuartzDetail())
.withIdentity("testQuartz")
.withSchedule(scheduleBuilder)
.build();
}
}

Quartz 设计的核心类包括 Scheduler,Job 以及Trigger。Job 负责定义需要执行的任务,Trigger 负责设置调度策略,Scheduler 将二者组装在一起,并触发任务开始执行。

Job
使用者只需要创建一个 Job 的继承类,实现 execute 方法。JobDetail 负责封装 Job 以及 Job 的属性,并将其提供给 Scheduler 作为参数。每次 Scheduler 执行任务时,首先会创建一个 Job 的实例,然后再调用 execute 方法执行。Quartz 没有为 Job 设计带参数的构造函数,因此需要通过额外的 JobDataMap 来存储 Job 的属性。JobDataMap 可以存储任意数量的 Key,Value 对:

Trigger
用于设置调度策略。Quartz 设计多种类型的 Trigger,最常用的是 SimpleTrigger 和 CronTrigger。

  • SimpleTrigger 适用于在某一特定的时间执行一次,或者在某一特定的时间以某一特定时间间隔执行多次。上述功能决定SimpleTrigger 的参数包括 start-time,end-time,repeat count,repeat interval。
    Repeat count 取值为>=0的整数,或常量 SimpleTrigger.REPEAT_INDEFINITELY。
    Repeat interval 取值为>=0的长整型。当 Repeat interval 取值为零并且 Repeat count 取值大于零时,将会触发任务的并发执行。
    Start-time 与 end-time 取值为​​​java.util.Date​​。同时指定 end-time 与 repeat count 时,优先考虑 end-time。一般地,可以指定 end-time,并设定 repeat count 为 REPEAT_INDEFINITELY。
  • CronTrigger,用途更广,相比基于特定时间间隔进行调度安排的 SimpleTrigger,适用于基于日历的调度安排,需要指定 start-time 和 end-time,Cron 表达式,由七个字段组成:Seconds、Minutes、Hours、Day-of-Month、Month、Day-of-Week、Year (Optional field)。

举例:
每十分钟执行的 CronTrigger,且从每小时的第三分钟开始执行:​​​0 3/10 * * * ?​​​ 每周一,周二,周三,周六的晚上 20:00 到 23:00,每半小时执行一次的 CronTrigger:​​0 0/30 20-23 ? * MON-WED,SAT​​ 每月最后一个周四,中午 11:30-14:30,每小时执行一次的 trigger:​​0 30 11-14/1 ? * 5L​

首先所有字段都有自己特定的取值,Seconds 和 Minutes 取值为 0 到 59,Hours 取值为 0 到 23,Day-of-Month 取值为 0-31,Month 取值为 0-11,或JAN…DEC,Days-of-Week 取值为 1-7 或 SUN…SAT。每个字段可以取单个值,多个值,或一个范围,如 Day-of-Week 可取值为"TUE-THU,SUN"。
通配符​​​*​​​表示该字段可接受任何可能取值。
​​​/​​​表示开始时刻与间隔时段。
​​​?​​​仅适用于 Day-of-Month 和 Day-of-Week。​​?​​​表示对该字段不指定特定值。适用于需要对这两个字段中的其中一个指定值,而对另一个不指定值的情况。一般情况下,这两个字段只需对一个赋值。
​​​L​​​:仅适用于 Day-of-Month 和 Day-of-Week。L 用于 Day-of-Month 表示该月最后一天。L 单独用于 Day-of-Week 表示周六,否则表示一个月最后一个星期几,例如 5L 或者 THUL 表示该月最后一个星期四。
​​​W​​​:仅适用于 Day-of-Month,表示离指定日期最近的一个工作日,例如 Day-of-Month 赋值为 10W 表示该月离 10 号最近的一个工作日。
仅适用于 Day-of-Week,表示该月第X个星期几。例如 Day-of-Week 赋值为 5#2 或者 THU#2,表示该月第二个星期四。

Job 与 Trigger 的松耦合设计,其优点在于同一个 Job 可以绑定多个不同的 Trigger,同一个 Trigger 也可以调度多个 Job,灵活性很强。

Listener
Quartz提供Listener功能:JobListener,TriggerListener 以及 SchedulerListener。Listener用于当系统发生故障,通知到配置的告警人。当任务被执行时,系统发生故障,Listener 监听到错误,立即发送邮件给管理员。

参考

​​几种任务调度的Java实现方法与比较​​

​​https://mp.weixin.qq.com/s/-BsSivMrLrmz5zBR6DgK8Q​​


举报

相关推荐

0 条评论