上一篇文章中,我们分析了Quartz框架的Job
和Trigger
的源码实现,上篇也说到,Quartz的核心代码是Scheduler
,在本篇中,我们会分析一下Scheduler
的源码实现。
文章目录
1、核心入口类:QuartzScheduler
现在我们有了包装好的JobDetail
对象,有了具有时间表规则的Trigger
对象,只需要将这两个对象提交给Scheduler
就可以让Quartz框架按照我们定义的时间规则执行Job了。
其实到这里,我们已经发现,无论是Job
还是Tigger
以及他们的各种Builder
,都没有真正的执行逻辑,相当于沾板(配菜切菜)在处理食材,真正开始烹饪还是得靠大厨—Scheduler
。
Scheduler
采用工厂模式创建,这里我们直接看Quartz中的核心实现类QuartzScheduler
,这个类可以说是Quartz的心脏,话不多说,我们先看这个类的介绍:
* <p>
* This is the heart of Quartz, an indirect implementation of the
* <code>{@link org.quartz.Scheduler}</code>
* interface, containing methods to schedule
* <code>{@link org.quartz.Job}</code>s,
* register <code>{@link org.quartz.JobListener}</code> instances, etc.
* </p>
这个类是Scheduler的一个实现类,具有调度Job注册到JobListener实例的方法。
这个类中包含两个重要的字段,如下:
// Scheduler需要的资源类,包括工作线程池和JobStore存储Job和Trigger映射关系
private QuartzSchedulerResources resources;
// Shceduler调度线程,这个线程负责根据Trigger的出发时间触发Job执行
private QuartzSchedulerThread schedThread;
这两个类也是很重要功能组成部分,QuartzSchedulerResources
类是一个资源类,从类名也能看出来,这里先大致了解它的作用,这个类里面有工作线程需要用的线程池,以及上一篇中我们提到的JobStore
对象。
其次就是QuartzSchedulerThread
类,这个类是调度任务的核心类,整体Quartz框架来看,是由一条单独的线程来进行任务的调度,就像我一开始想的,一条线程不断轮询来判断各个Trigger
触发时间,然后找到Trigger
对应的Job
去运行。
2、调度线程类:QuartzSchedulerThread
上面说到了QuartzSchedulerThread
类,我们先来看一下QuartzScheduler
的构造方法:
可以看到,在构造函数调用时,就已经启动了QuartzSchedulerThread
调度线程,相当于此时刚创建了Scheduler
对象,还没有任何任务提交过来,这个线程已经启动了。
QuartzSchedulerThread
的核心是run()
方法,QuartzSchedulerThread
比我想象中实现的更为直接,并没有任务队列,而是任务直接提交到可用的工作线程上,这也是为了任务的实时性(想想我那种想法也不太行,比较人家是定时任务,万一在队列里等了半天没执行,那不就完全违背了定时任务的设计理念了吗)
我们抽出run()
方法中核心代码进行分析。
首先说明一点,run()
方法是一个持续的while
循环,直到这个Scheduler
关闭,这样才能满足QuartzSchedulerThread
作为一个调度线程的功能。下面看一段重要源码:
// 获得可用的worker线程数量
int availThreadCount = qsRsrcs.getThreadPool().blockForAvailableThreads();
// 可用的worker线程数量大于0时,才提交任务
if(availThreadCount > 0) { // will always be true, due to semantics of blockForAvailableThreads...
List<OperableTrigger> triggers;
// 当前系统时间
long now = System.currentTimeMillis();
clearSignaledSchedulingChange();
try {
// 这里是核心代码,acquireNextTriggers()方法会返回将要触发的Trigger
// 这里根据一个将来短暂的时间段来判断,返回在这个时间段内会触发的Trigger列表
triggers = qsRsrcs.getJobStore().acquireNextTriggers(
now + idleWaitTime, Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()), qsRsrcs.getBatchTimeWindow());
acquiresFailed = 0;
} catch (JobPersistenceException jpe) {
// 省略代码…
continue;
} catch (RuntimeException e) {
// 省略代码…
continue;
}
这里Quartz的机制是,从JobStore
对象中拿到下一组要触发的Tigger
对象,也就是说拿到未来一个小时间段内将要触发的任务,而不是只拿到现在需要执行的任务。
我们知道,目前拿到的Trigger
列表只是未来一段时间内要执行的Trigger
,他们的触发时机必定是一个特定的时间点。
从逻辑上分析一下,这里必然要拿到未来最早要触发的任务时间,才是最合理的。
那继续分析下一段关键代码,看Quartz是怎么处理的。
if (triggers != null && !triggers.isEmpty()) {
// 获取当前系统时间
now = System.currentTimeMillis();
// 获取最近要触发的任务的触发时间
long triggerTime = triggers.get(0).getNextFireTime().getTime();
long timeUntilTrigger = triggerTime - now; // 获取触发倒计时
while(timeUntilTrigger > 2) {
// 这里判断依据是2毫秒,其实是因为线程休眠唤醒也是需要一定时间的
// 因此如果触发倒计时小于2毫秒 ,就认为这时已经可以触发任务了
synchronized (sigLock) {
if (halted.get()) {
break;
}
// 这里是判断是否需值得休眠,因为有可能存在Scheduler被暂停(paused)
// 被终止(halt)的情况,这时候就没必要休眠了
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)
// 这里是让当前线程等待,等待时间就是触发倒计时
// 让线程等待的原因是防止持续while循环导致cpu占用过高
sigLock.wait(timeUntilTrigger);
} catch (InterruptedException ignore) {
}
}
}
if(releaseIfScheduleChangedSignificantly(triggers, triggerTime)) {
break;
}
now = System.currentTimeMillis();
timeUntilTrigger = triggerTime - now;
}
可以看到,这里其实就是等待到达下一个触发时间点,等待的时间段内,Quartz让线程wait
指定时间,被唤醒以后继续向下运行,被唤醒的时候相当于到达了Trigger
触发时间点(可能会有毫秒级别的误差)。
唤醒以后的线程,此时到达了Trigger
触发的时间点,下一步很明显就是要执行Trigger
关联的任务了。
List<TriggerFiredResult> bndles = new ArrayList<TriggerFiredResult>();
boolean goAhead = true;
synchronized(sigLock) {
// 再次判断任务是否已经取消
goAhead = !halted.get();
}
if(goAhead) {
try {
// 这里获得的TriggerFiredResult是一个包装过的JobDetail和Trigger对象
// 也就是找到了Trigger关联的Job
List<TriggerFiredResult> res = qsRsrcs.getJobStore().triggersFired(triggers);
if(res != null)
bndles = res;
} catch (SchedulerException se) {
qs.notifySchedulerListenersError(
"An error occurred while firing triggers '"
+ triggers + "'", se);
//QTZ-179 : a problem occurred interacting with the triggers from the db
//we release them and loop again
for (int i = 0; i < triggers.size(); i++) {
// 出现异常,就释放这些准备执行的Trigger,重新放回等待列表
qsRsrcs.getJobStore().releaseAcquiredTrigger(triggers.get(i));
}
continue;
}
}
现在我们已经拿到了当前时间点需要触发的Trigger
以及其关联的JobDetail
,下一步就是把Job提交给Quartz的WorkerThreadPool
(工作线程池),肯定不能在Scheduler
的线程里去执行任务,因为Scheduler
线程只是作为一个时间调度线程,需要保证实时性而且不能被阻塞,因此任务工作线程就必然是额外的线程。
for (int i = 0; i < bndles.size(); i++) {
TriggerFiredResult result = bndles.get(i);
TriggerFiredBundle bndle = result.getTriggerFiredBundle();
Exception exception = result.getException();
/*
省略代码……
*/
if (bndle == null) {
qsRsrcs.getJobStore().releaseAcquiredTrigger(triggers.get(i));
continue;
}
JobRunShell shell = null;
try {
// 这里就是包装Job成为一个shell
shell = qsRsrcs.getJobRunShellFactory().createJobRunShell(bndle);
shell.initialize(qs);
} catch (SchedulerException se) {
// 省略代码……
continue;
}
// 核心代码:提交job到Worker线程池执行任务
if (qsRsrcs.getThreadPool().runInThread(shell) == false) {
getLog().error("ThreadPool.runInThread() return false!");
qsRsrcs.getJobStore().
triggeredJobComplete(triggers.get(i), bndle.getJobDetail(),
CompletedExecutionInstruction.SET_ALL_JOB_TRIGGERS_ERROR);
}
}
到这里,Scheduler
作为时间调度器的代码实际上已经完毕,下面就是WorkerThread
去执行任务的代码了。
3、任务执行线程池:SimpleThreadPool
Quartz框架默认实现了一个简单线程池SimpleThreadPool
类,也就是说Worker
线程池是Quartz框架实现的,而不是直接使用的Java线程池(因为要满足任务执行的时效性,Java线程池设计上有一种异步的理念,不太适合Quartz使用)
先来看一下SimpleThreadPool
类结构图,其中有一个内部类WorkerThread
,是一个继承自Java.lang.Thread
的类,当然Quartz为了实现其工作线程的功能,重写了run()
方法,这个我们一会再进行分析,SimpleThreadPool
类其他一些属性就是用来标志一些线程池的属性。
我们来分析一下SimpleThreadPool
的原理,首先来看其初始化方法:
public void initialize() throws SchedulerConfigException {
/*
省略部分代码,主要是错误处理的代码……
*/
// 这里count是从配置文件中得到的线程数量,默认是10个工作线程,会创建10个线程
Iterator<WorkerThread> workerThreads = createWorkerThreads(count).iterator();
while(workerThreads.hasNext()) {
WorkerThread wt = workerThreads.next();
wt.start(); // 直接启动线程
availWorkers.add(wt); // 添加到线程池的可用线程列表中
}
}
可以看到,工作线程池的初始化方法很容易理解,就是循环创建了多个工作线程并启动他们,这里有个细节,就是工作线程池是在SchedulerFactory
工厂创建Scheduler
对象的时候就已经创建了,也就是说还早于调度线程QuartzSchedulerThread
的创建。
下面我们分析一下SimpleThreadPool
线程池中执行任务时调用的runInThread
方法。
public boolean runInThread(Runnable runnable) {
if (runnable == null) {
return false;
}
// 这里单纯是为了wait使用,没有并发安全问题
synchronized (nextRunnableLock) {
handoffPending = true;
// Wait until a worker thread is available
// 如果当前没有空闲工作线程,就等待500毫秒
while ((availWorkers.size() < 1) && !isShutdown) {
try {
nextRunnableLock.wait(500);
} catch (InterruptedException ignore) {
}
}
if (!isShutdown) {
// 这里是正常运行任务的流程,同时把线程从空闲线程队列移动到工作线程队列
WorkerThread wt = (WorkerThread)availWorkers.removeFirst();
busyWorkers.add(wt);
wt.run(runnable); // 执行任务
} else {
// If the thread pool is going down, execute the Runnable
// within a new additional worker thread (no thread from the pool).
// 这里是一种补救机制,如果线程池已经关闭,但是此时任务已经被提交
// 就单独开一个线程来执行任务(此时其他工作线程可能 正在/已经 销毁)
WorkerThread wt = new WorkerThread(this, threadGroup,
"WorkerThread-LastJob", prio, isMakeThreadsDaemons(), runnable);
busyWorkers.add(wt);
workers.add(wt);
wt.start();
}
nextRunnableLock.notifyAll();
handoffPending = false;
}
return true;
}
在上面QuartzSchedulerThread
调用的就是SimpleThreadPool
的runInThread
方法。
通过源码分析,我们很明显能感觉到,SimpleThreadPool
中的runInTread
方法,其实非常像Java线程池中的execute()
方法,都是提交一个任务到线程池中,只不过SimpleThreadPool
会保证这个任务会被立即执行(如果不能立即执行就等待到执行为止),而不是像Java线程池会提交到一个等待队列,让线程池慢慢消化。这也体现了Quartz框架的特点,就是任务执行的时效性。
4、任务执行线程类:WorkerThread
来看一下WorkerThread
类的内部结构,可以看到有两个run()
方法,我们知道实现Runnable
接口需要实现run()
方法,这里它还有一个run(Runnable)
方法,其实这个方法名起的很差,这个方法本质上是 提交/执行 一个任务,不如改为submit() / execute()
更让人清楚。
我们来看一下SimpleThreadPool.runInThread
使用的run(Runnable)
方法,其实就是把WorkerThread
需要执行的任务提交进来。
public void run(Runnable newRunnable) {
synchronized(lock) {
if(runnable != null) {
throw new IllegalStateException("Already running a Runnable!");
}
// runnable是类属性
runnable = newRunnable;
lock.notifyAll(); // 主动唤醒处于等待状态的线程
}
}
那么继承Thread
类重写的run()
方法又是干什么的?很明显这个应该是工作线程真实工作时的方法。
* <p>
* Loop, executing targets as they are received.
* 意思很简答:循环,执行接收到的任务
* </p>
*/
@Override
public void run() {
boolean ran = false;
// 这里时判断是否要关闭线程池,如果关闭就退出循环,销毁线程
while (run.get()) {
try {
synchronized(lock) {
// 如果没有任务需要执行,就进入等待状态
// 这样做的原因是减少空循环带来的cpu资源销毁
while (runnable == null && run.get()) {
lock.wait(500); // 等待500ms
}
if (runnable != null) {
ran = true;
runnable.run(); // 执行提交的任务
}
}
} catch (InterruptedException unblock) {
// do nothing (loop will terminate if shutdown() was called
try {
getLog().error("Worker thread was interrupt()'ed.", unblock);
} catch(Exception e) {
// ignore to help with a tomcat glitch
}
} catch (Throwable exceptionInRunnable) {
try {
getLog().error("Error while executing the Runnable: ",
exceptionInRunnable);
} catch(Exception e) {
// ignore to help with a tomcat glitch
}
} finally {
synchronized(lock) {
runnable = null;
}
// repair the thread in case the runnable mucked it up...
if(getPriority() != tp.getThreadPriority()) {
setPriority(tp.getThreadPriority());
}
// 这里是我上面说的补救情况,就是线程池处于关闭情况,此时一个任务被提交进来
// 就单独启动一个线程完成提交的任务,执行完成以后线程销毁
if (runOnce) {
run.set(false);
clearFromBusyWorkersList(this);
} else if(ran) {
ran = false;
makeAvailable(this); // 从工作队列中重新放入空闲队列
}
}
}
//if (log.isDebugEnabled())
try {
getLog().debug("WorkerThread is shut down.");
} catch(Exception e) {
// ignore to help with a tomcat glitch
}
}
}
可以看到这个工作线程类的是实现方法,比起Java线程池来说简单了不知道多少倍,就是不断地循环,等待任务传入,然后执行,从线程池的角度来看,是一个同步阻塞的模型。
5、总结:
到这里为止,我们分析完了Quartz的核心执行任务的流程,其中一些思想还是值得学习的,比如说线程模型,比如说任务调度算法。
当然,Quartz中还有很多代码,比如说Cron
表达式的解析转换逻辑,比如说数据库存储分布式部署方式等等很多代码,我们就不一一分析了,有兴趣的小伙伴可以自己去查看。
当然,源码分析中可能存在一些错误,因为这些都是我自己阅读的,当然其中还有一些没看懂的部分,希望大家能批评指正。
源码分析类的文章太费时间了,而且从点击率来看,远远低于一些Java基础/工具使用的文章,看来要搞一些新手教程向的文章,提高提高博客的点击率了。
最后,希望大家都活成一个软件开发工程师,而不是一个码农。保持好奇,保持学习。