Quartz中核心的调度类是QuartzScheduler,任务的调度和任务的管理都是QuartzScheduler实现的,然后通过一个静态代理类StdScheduler提供出来。所以要理解定时任务是如何触发的,我们只需要理解透QuartzScheduler即可。
QuartzScheduler类
public class QuartzScheduler implements RemotableQuartzScheduler {
private QuartzSchedulerResources resources;
private QuartzSchedulerThread schedThread;
private SchedulerSignaler signaler;
private boolean signalOnSchedulingChange = true;
private volatile boolean closed = false;
private volatile boolean shuttingDown = false;
private Date initialStart = null;
/** Update timer that must be cancelled upon shutdown. */
private final Timer updateTimer;
......
上面是QuartzScheduler比较重要的几个成员,从字面上都很好理解,我们重点关注QuartzSchedulerThread这个成员,它就是负责处理触发定时任务的线程类!
QuartzSchedulerThread
public class QuartzSchedulerThread extends Thread {
private QuartzScheduler qs;
private QuartzSchedulerResources qsRsrcs;
private final Object sigLock = new Object(); // 信号锁,任何对scheduler的操作都要上锁
private boolean signaled; // 是否已通知
private long signaledNextFireTime; // 定时任务下次触发时间
private boolean paused;// 是否进入standby模式
private AtomicBoolean halted;// 是否停止
private Random random = new Random(System.currentTimeMillis());
// When the scheduler finds there is no current trigger to fire, how long
// it should wait until checking again...
private static long DEFAULT_IDLE_WAIT_TIME = 30L * 1000L;
private long idleWaitTime = DEFAULT_IDLE_WAIT_TIME;
private int idleWaitVariablness = 7 * 1000;
......
我们重点看它的run方法,run方法一来就是一个巨大的while循环:
while (!halted.get()) {
......
}
halted是一个原子boolean类,它指示scheduler任务调度是否停止,初始化时为false,只有shutdown的时候才会设置为true。
紧接着是一个同步块:
// check if we're supposed to pause...
synchronized (sigLock) {
while (paused && !halted.get()) {
try {
// wait until togglePause(false) is called...
sigLock.wait(1000L);
} catch (InterruptedException ignore) {
}
}
if (halted.get()) {
break;
}
}
如果scheduler没有停止任务调度,当处于standby模式(通过调用standby()方法,paused=true)时,就会wait 1秒钟,直到调用了shutdown()、start()等方法,改变了scheduler状态。当Scheduler创建的时候,最初是处于standby模式,我们集成quartz到spring的时候,会有一个手动调用start()启动Scheduler任务调度的一个步骤,具体可以参考之前写过的spring集成quartz的文章。
接下来是获取触发器,
triggers = qsRsrcs.getJobStore().acquireNextTriggers(
now + idleWaitTime,
Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()),
qsRsrcs.getBatchTimeWindow());
这里面三个参数非常重要,有必要做一下解释:
now+idelWaitTime:这是一个现在的时刻加上了一个空闲时间,默认30秒,它和后面的timeWindow(默认0秒)组成了我们要获取的待触发的触发器的时间范围,即下次触发时间在now~now+30+0的时间范围内的触发器。
Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()):这是我们要从数据库获取的触发器个数,取线程池可用线程数和配置的定时任务批量处理个数的最低值。maxBatchSize默认值是1,因此一般情况下我们取到的trigger只有一个。
batchTimeWindow:这是一个时间窗口,默认值0。解释参考idleWatiTime。
上面几个参数都可以在quartz.properties中进行配置。
关于获取触发器
获取触发器的实际操作中,其实是有一个配置(例如创建CronScheduleBuilder的时候,指定了过期策略,默认是立即触发一次)。那么获取的触发器除了将要触发的触发器,还包括过期的触发器。但是这个过期有个时间范围,默认是10秒,也就是过了下次触发时间10秒之内的触发器我们也算作合法的触发器。综上,我们获取的触发器就包括在将来一段时间内要触发的触发器,和刚过期一小会儿的触发器。
获取到的触发器是按照触发时间升序排序,然后按照优先级降序排序。然后就是以第一个触发器的触发时间为准开始处理触发器
now = System.currentTimeMillis();
long triggerTime = triggers.get(0).getNextFireTime().getTime();
long timeUntilTrigger = triggerTime - now;
while(timeUntilTrigger > 2) {
synchronized (sigLock) {
if (halted.get()) {
break;
}
if (!isCandidateNewTimeEarlierWithinReason(triggerTime, false)) {
try {
// we could have blocked a long while
// on 'synchronize', so we must recompute
now = System.currentTimeMillis();
timeUntilTrigger = triggerTime - now;
if(timeUntilTrigger >= 1)
sigLock.wait(timeUntilTrigger);
} catch (InterruptedException ignore) {
}
}
}
if(releaseIfScheduleChangedSignificantly(triggers, triggerTime)) {
break;
}
now = System.currentTimeMillis();
timeUntilTrigger = triggerTime - now;
}
如果等待时间较长,而且没有新的定时任务加入、重启等等,线程就会wait一段时间。然后继续判断是否有新的情况(新任务加入,重启等等),如果有则释放掉已经获取到的触发器,包括将状态重新置为WAITING,然后在FIRED_TRIGGERS表中删除该记录,然后重新开始循环。如果没有则直到等待时间完毕,开始进行触发操作:
List<TriggerFiredResult> res = qsRsrcs.getJobStore().triggersFired(triggers);
触发操作主要包括更新FIRED_TRIGGERS和TRIGGERS两张表:
1-更新FIRED_TRIGGERS,更新相关触发时间,并将状态置为EXECUTING。
2-更新TRIGGERS,保存相关更改。
3-另外,如果不允许并发执行,则将原来等待,已经获取,暂停的触发器状态置为阻塞状态,表明不能再次对其进行触发。
总结
定时任务的触发,基本上都是对各种trigger进行操作,更新trigger数据和状态。
为什么获取触发器会有一个时间范围
更好的一个提问方式是:为什么时间范围内触发器都会被触发?
我们获取的触发器是一个时间范围内的触发器列表,而且是按照最早的一个触发器的触发时间来执行任务的。那么在这个范围内(一般是30秒),而又不在这个时间点的触发器被触发了,那不是不符合要求了?
其实,我们在获取触发器的时候,有一个maxBatchSize的参数,这个参数默认值是1,它就保证了我们最终获取到的只有一个触发器,恰好就是那个最先触发的触发器(大家可以试着将它设置的大一点,然后几个定时任务之间间隔不超过30秒,然后测试一下)。其它时间范围内的触发器都不会被处理。也就是说,一个循环内最多只会有一个trigger被处理,即使它们的触发时间是同一个时间点。如果需要支持多个trigger,而且对时间点要求的又不是那么高的话,我们可以通过两个参数设置:
org.quartz.scheduler.idleWaitTime : 3000
org.quartz.scheduler.batchTriggerAcquisitionMaxCount:5
我们可以把idleWaitTime设置得小一些来缩小时间误差,然后我们就可以将batchTriggerAcquisitionMaxCount设置的大一点,让schedulerThread可以同时处理更多的trigger。
需要注意的是,如果你的定时任务需要在准确的时间点触发(精确到秒,不过这样的定时任务很少,谁知道呢?),那么最好这两个参数都不要动,特别是batchTriggerAcquisitionMaxCount。因为它等于1,才能确保定时任务在准确的时间点触发。