前些天看了Elastic-Job的实现原理,它是基于Quartz实现的,Quartz提供了定时任务的功能,现在我们就来看看Quartz底册是怎么实现的。初始化部分我们就不关注了,直接进入核心部分:
scheduler.start();
启动定时器
public void start() throws SchedulerException {
if (shuttingDown|| closed) {
throw new SchedulerException(
"The Scheduler cannot be restarted after shutdown() has been called.");
}
// QTZ-212 : calling new schedulerStarting() method on the listeners
// right after entering start()
notifySchedulerListenersStarting();
if (initialStart == null) {
initialStart = new Date();
this.resources.getJobStore().schedulerStarted();
startPlugins();
} else {
resources.getJobStore().schedulerResumed();
}
schedThread.togglePause(false);
getLog().info(
"Scheduler " + resources.getUniqueIdentifier() + " started.");
notifySchedulerListenersStarted();
}
首先检查了定时器的状态,如果当前是shuttingDown或者closed状态,那么就抛出异常,然后唤醒定时器里面的监听器执行他们的定时器正在启动的监听方法,如果是第一次启动,那么初始化initialStart和执行schedulerStarted()方法,这个方法是一个空方法什么也没做,接着启动插件,如果不是第一次启动,那么执行schedulerResumed()方法,这个方法也是一个空方法。下面给schedThread的一个字段设置为false,最后执行监听器的启动完毕的方法。
我们发现在start方法中有一个线程schedThread,那么我们就先来看一下它是如何被创建的
QuartzSchedulerThread(QuartzScheduler qs, QuartzSchedulerResources qsRsrcs, boolean setDaemon, int threadPrio) {
super(qs.getSchedulerThreadGroup(), qsRsrcs.getThreadName());
this.qs = qs;
this.qsRsrcs = qsRsrcs;
this.setDaemon(setDaemon);
if(qsRsrcs.isThreadsInheritInitializersClassLoadContext()) {
log.info("QuartzSchedulerThread Inheriting ContextClassLoader of thread: " + Thread.currentThread().getName());
this.setContextClassLoader(Thread.currentThread().getContextClassLoader());
}
this.setPriority(threadPrio);
// start the underlying thread, but put this object into the 'paused'
// state
// so processing doesn't start yet...
paused = true;
halted = new AtomicBoolean(false);
}
首先保存了定时器的一些信息,设置是否是守护线程,设置优先级,最后初始化了两个标志,都是停止的意思,并且,我们在刚才的start方法里面,发现出现了一次给paused的赋值,这个线程是在声明定时器的时候被启动的,那么,我们就来看一下它的run方法做了什么
public void run() {
boolean lastAcquireFailed = false;
while (!halted.get()) {
...
}
}
从宏观上看,他的run方法在执行一个死循环,只要halted为false就会一直执行下去,接下来,我们深入到内部去看一下
synchronized (sigLock) {
while (paused && !halted.get()) {
try {
// wait until togglePause(false) is called...
sigLock.wait(1000L);
} catch (InterruptedException ignore) {
}
}
if (halted.get()) {
break;
}
}
是不是发现了什么不可思议的事,在这段代码里面出现了paused标志,我们可以发现,如果paused如果为false,这个线程又会陷入这个死循环,所以在start方法里面,将paused字段设置为false的时候,就让线程跳出了这个循环向下执行,也算是真正的启动了这个线程
int availThreadCount = qsRsrcs.getThreadPool().blockForAvailableThreads();
当线程获取的可以执行的定时任务的时候,会将任务放到一个线程池中去执行,在这一步会获取当前线程池空闲的线程的数目,并作为下面获取定时任务数目的参考,先来看一下是如何获取空闲线程的数目的
public int blockForAvailableThreads() {
synchronized(nextRunnableLock) {
while((availWorkers.size() < 1 || handoffPending) && !isShutdown) {
try {
nextRunnableLock.wait(500);
} catch (InterruptedException ignore) {
}
}
return availWorkers.size();
}
}
可以看到,这是一个阻塞的方法,如果当前线程池里面没有空闲的线程,那么阻塞到空闲线程的数目大于等于1或者线程池关闭,否则返回空闲线程的数目
if(availThreadCount > 0) {
...
}
else { // if(availThreadCount > 0)
// should never happen, if threadPool.blockForAvailableThreads() follows contract
continue; // while (!halted)
}
获取完空闲线程的数目后,根据数目的大小进行不同的操作,如果为0,那么重新循环,我们重点关注的是由空闲线程的那部分
List<OperableTrigger> triggers = null;
long now = System.currentTimeMillis();
clearSignaledSchedulingChange();
首先声明了一个集合用来保存获取到的定时任务,下一行是获取当前时间,最后一个方法用来清除标志,这个标志代表:当从定时任务的集合中获取定时任务或者获取完定时任务后有新插入的定时任务,就需要设置一些标志来告诉当前线程有新的定时任务插入进来了,需要根据新插入的定时任务的下次触发时间进行相应的操作
public void clearSignaledSchedulingChange() {
synchronized(sigLock) {
signaled = false;
signaledNextFireTime = 0;
}
}
signaled 用来标志有新插入的定时任务,signaledNextFireTime用来记录新插入的定时任务的下次触发时间
try {
triggers = qsRsrcs.getJobStore().acquireNextTriggers(
now + idleWaitTime, Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()), qsRsrcs.getBatchTimeWindow());
lastAcquireFailed = false;
if (log.isDebugEnabled())
log.debug("batch acquisition of " + (triggers ==