Java基础之定时任务调度
概述
任务调度是指基于给定时间点,给定时间间隔或者给定执行次数自动执行任务。在Java里面的有很多工具可供使用:
- Timer:用得很少
- ScheduledExecutorService
- Spring SchedulingTaskExecutor
- 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还在执行,那么这个程序就会一直在运行。
终止一个定时器主要有一下三种方法:
- 调用
timer.cancel()
方法,可以在程序的任何地方调用此方法,甚至可以在TimerTask的run方法里使用此方法; - 让timer定时器成为一个守护进程,这样当程序只有守护进程存在时,守护进程就会停止运行,程序自然也会停止,而让timer定时器成为一个守护进程的方法是使用Timer的timer(true)构造方法;
- 调用
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存在一些缺陷:
- Timer对调度的支持是基于绝对时间的,而不是相对时间,所以它对系统时间的改变非常敏感。Timer在执行定时任务时只会创建一个线程任务,如果存在多个线程,若其中某个线程因为某种原因而导致线程任务执行时间过长,超过两个任务的间隔时间,会发生一些缺陷
- 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的缺陷:
- Timer 对提交的任务调度是基于绝对时间而不是相对时间,通过其提交的任务对系统时钟的改变是敏感的(譬如提交延迟任务后修改系统时间会影响其执行);而SES只支持相对时间,对系统时间不敏感。
- 因为 Timer 线程并不捕获异常,所以 TimerTask 抛出的未检查异常会使 Timer 线程终止,后续提交的任务得不到执行,将会产生无法预料的行为;而STPE不存在此问题。
类图:
接口源码:
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
参考
https://mp.weixin.qq.com/s/-BsSivMrLrmz5zBR6DgK8Q