Quartz学习笔记
最近项目中要用到作业调度的功能,很自然想到大名鼎鼎的Quartz。但实际用的时候碰到一个很蛋疼的问题,自己定义的作业始终触发不了,而且日志上也没有异常抛出来。虽然最终问题解决了,而且问题的原因很操蛋,但还是把过程中自己对Quartz的一点点拙见理解写下来,一来方便以后复习,而来本着分享精神。
Quartz中真正干活的几个类如下:
QuartzScheduler 任务调度器(内部使用)
TreadPool Quartz线程池,干活的线程就是从这里分配出去并管理的。
因为比较懒,这里直接使用默认的SimpleTreadPool
QuartzSchedulerThread Quartz主线程。就是他负责找到需要出发的作业,并交给TreadPool执行
JobStore 贮存器,负责提供JobDetail和Trigger。
同样因为比较懒,这里直接使用的RAMStore
Trigger 触发器父类,负责控制作业的出发时间。这里使用的是CronTrigger
SchedulerFactoryBean Spring与Quartz的一个连接类,负责Quartz的初始化和启动工作。
该类实现了InitializingBean,SmartLifecycle接口。所以初始化和启动是由Spring负责调用的。
Quartz的原理大致如下:
IOC容器初始化时(我是用Spring与Quartz结合的)会创建并初始化Quartz线程池(TreadPool),并启动它。刚启动时线程池中每个线程都处于等待状态,等待外界给他分配Runnable(持有作业对象的线程)。
然后会初始化并启动Quartz的主线程(QuartzSchedulerThread),该线程自启动后就会等待外界的信号量开始工作。外界给出工作信号量之后,该主线程的run方法才实质上开始工作。run中会获取JobStore中下一次要触发的作业,拿到之后会一直等待到该作业的真正触发时间,然后将该作业包装成一个JobRunShell对象(该对象实现了Runnable接口,其实看是上面TreadPool中等待外界分配给他的Runnable),然后将刚创建的JobRunShell交给线程池,由线程池负责执行作业。
线程池收到Runnable后,从线程池一个线程启动Runnable,然后将该线程回收至空闲线程中。
JobRunShell对象的run方法就是最终通过反射调用作业的地方。
源码分析过程大致如下(我只看了上面的几个类,具体的类似配置文件读取、Listener实现之类的就没怎么看了。有兴趣的可以自己看下):
因为我这里的Quartz是与Spring结合使用的,所以初始化的入口是SchedulerFactoryBean。该类实现了Spring的InitializingBean接口,所以IOC容器初始化完成后会调用afterPropertiesSet方法。Quartz的初始化也是在这里完成的。又因为该类实现了Spring的SmartLifecycle接口,所以真正启动主线程的start方法也是由Spring调用的。
public void afterPropertiesSet() throws Exception {
/**
* schedulerFactoryClass默认是StdSchedulerFactory,initSchedulerFactory方法没有仔细看,应该是读取配置信息
*/
SchedulerFactory schedulerFactory = (SchedulerFactory)BeanUtils.instantiateClass(this.schedulerFactoryClass);
initSchedulerFactory(schedulerFactory);
。。。
// 所有的工作都是在createScheduler方法中做的:创建线程池、创建并启动主线程。
// 但这里创建的主线程并没有实质上的开始工作,他要等待外界的信号量
try {
this.scheduler = createScheduler(schedulerFactory, this.schedulerName);
populateSchedulerContext();
}
。。。
// registerListeners注册监听器,这个方法没有仔细看过
// registerJobsAndTriggers方法就是读取配置的作业和他们的触发器的地方
registerListeners();
registerJobsAndTriggers();
}
跟踪createScheduler方法(这里返回的Scheduler对象就是最终要返回的Scheduler任务调度者):
protected Scheduler createScheduler(SchedulerFactory schedulerFactory, String schedulerName)
throws SchedulerException {
。。。
// 这里创建的是StdScheduler,调用方法的自然也是StdSchedulerFactory
Scheduler newScheduler = schedulerFactory.getScheduler();
。。。
}
跟踪StdSchedulerFactory的getScheduler方法:
public Scheduler getScheduler() throws SchedulerException {
// 比较关键的就instantiate方法,其他的就是加载配置信息,判断缓存里有没有意见创建过的Scheduler等等
。。。
sched = instantiate();
。。。
}
跟踪instantiate方法:
这个方法很长很长,我这里指截取其中某写片段进行说明。
private Scheduler instantiate() throws SchedulerException {
。。。
// 这里就是创建线程池的地方。tpClass是默认是SimpleTreadPool,具体的下面会分析
String tpClass = cfg.getStringProperty(PROP_THREAD_POOL_CLASS, null);
try {
tp = (ThreadPool) loadHelper.loadClass(tpClass).newInstance();
} catch (Exception e) {。。。}
tProps = cfg.getPropertyGroup(PROP_THREAD_POOL_PREFIX, true);
。。。
// 这里是创建JobStore的地方,负责保存作业和触发器。这里是默认的RAMJobStore
String jsClass = cfg.getStringProperty(PROP_JOB_STORE_CLASS, RAMJobStore.class.getName());
try {
js = (JobStore) loadHelper.loadClass(jsClass).newInstance();
} catch (Exception e) {。。。}
。。。
// 这里就是创建Quartz内部调度器和Quartz主线程的地方。主线程会在QuartzScheduler的构造函数中创建并启动
qs = new QuartzScheduler(rsrcs, schedCtxt, idleWaitTime, dbFailureRetry);
。。。
}
初始化的时候我关心的代码大概就这么多。下面具体跟踪看下
先从TreadPool的创建开始,这里创建的是SimpleTreadPool。SimpleTreadPool中持有3个List
private List workers; // 存放池中所有的线程引用
private LinkedList availWorkers = new LinkedList(); // 存放所有空闲的线程
private LinkedList busyWorkers = new LinkedList(); // 存放所有工作中的线程
public void initialize() throws SchedulerConfigException {
。。。
// 如果外界没有配置,那默认的线程组就是main线程的第一层子线程组
if(isThreadsInheritGroupOfInitializingThread()) {
threadGroup = Thread.currentThread().getThreadGroup();
} else {
// follow the threadGroup tree to the root thread group.
threadGroup = Thread.currentThread().getThreadGroup();
ThreadGroup parent = threadGroup;
while ( !parent.getName().equals("main") ) {
threadGroup = parent;
parent = threadGroup.getParent();
}
threadGroup = new ThreadGroup(parent, schedulerInstanceName + "-SimpleThreadPool");
if (isMakeThreadsDaemons()) {
threadGroup.setDaemon(true);
}
}
// createWorkerThreads方法中会根据配置的池大小创建线程实例。并启动池中每一个线程
// 这里启动的线程就是上面说到的等待Runnable(JobRunShell)的线程。
// create the worker threads and start them
Iterator workerThreads = createWorkerThreads(count).iterator();
while(workerThreads.hasNext()) {
WorkerThread wt = (WorkerThread) workerThreads.next();
wt.start();
availWorkers.add(wt);
}
}
跟踪createWorkerThreads方法:
// 池中实际的对象是WorkerThread对象。
protected List createWorkerThreads(int count) {
workers = new LinkedList();
for (int i = 1; i<= count; ++i) {
WorkerThread wt = new WorkerThread(this, threadGroup,
getThreadNamePrefix() + "-" + i,
getThreadPriority(),
isMakeThreadsDaemons());
if (isThreadsInheritContextClassLoaderOfInitializingThread()) {
wt.setContextClassLoader(Thread.currentThread().getContextClassLoader());
}
workers.add(wt);
}
return workers;
}
跟踪WorkerThread的run方法:
public void run() {
boolean ran = false;
boolean shouldRun = false;
synchronized(this) {
shouldRun = run;
}
while (shouldRun) {
try {
synchronized(this) {
// 放Runnable为空(外界还没有给JobRunShell)的时候,这个线程无限等待。
while (runnable == null && run) {
this.wait(500);
}
}
if (runnable != null) {
ran = true;
// 这里就是JobRunShell的run方法,也就是作业最终被调用的地方。
runnable.run();
}
} catch (InterruptedException unblock) {
try {
getLog().error("Worker thread was interrupt()'ed.", unblock);
} catch(Exception e) {}
} catch (Throwable exceptionInRunnable) {
try {
getLog().error("Error while executing the Runnable: ",
exceptionInRunnable);
} catch(Exception e) {}
} finally {
synchronized(this) {
runnable = null;
}
if(getPriority() != tp.getThreadPriority()) {
setPriority(tp.getThreadPriority());
}
if (runOnce) {
synchronized(this) {
run = false;
}
// 如果只执行一次则执行完成后该对象不放入空闲线程队列中
clearFromBusyWorkersList(this);
} else if(ran) {
ran = false;
// 将该对象从工作线程队列中删除,并且放入空闲队列中。这个方法实际上就是线程的回收
makeAvailable(this);
}
}
synchronized(this) {
shouldRun = run;
}
}
try {
getLog().debug("WorkerThread is shut down.");
} catch(Exception e) {
}
}
线程池的代码大概就是这样,下面跟踪QuartzScheduler的构造函数。这个类会创建Quartz的主线程。
public QuartzScheduler(QuartzSchedulerResources resources,
SchedulingContext ctxt, long idleWaitTime, long dbRetryInterval){
。。。
this.schedThread = new QuartzSchedulerThread(this, resources, ctxt);
。。。
}
QuartzSchedulerThread的构造函数中会将本身自启动,进入run的等待中。
关注QuartzSchedulerThread的run方法(这个run方法也是很长很长,这里只截取关键的部分):
public void run() {
while (!halted.get()) {
try {
synchronized (sigLock) {
// paused 就是等待外界的信号量,
// 需要信号量pausedc=false才能开始工作 QuartzScheduler.start()方法中会设置pausedc=false
while (paused && !halted.get()) {
try {
// wait until togglePause(false) is called...
sigLock.wait(1000L);
} catch (InterruptedException ignore) {
}
}
if (halted.get()) {
break;
}
// 当线程池中有空闲线程时才执行(这里也不是严格的,如果配置的没有空闲线程则创建一个新的)
int availTreadCount = qsRsrcs.getThreadPool().blockForAvailableThreads();
if(availTreadCount > 0) {
。。。
// 这里会找到下一个要触发的线程。具体的方法在下面会分析。
trigger = qsRsrcs.getJobStore().acquireNextTrigger(ctxt, now + idleWaitTime);
。。。等待线程到trigger的真正触发时间。。。
// 创建JobRunShell,要执行的作业就在这里面
JobRunShell shell = null;
try {
shell = qsRsrcs.getJobRunShellFactory().borrowJobRunShell();
shell.initialize(qs, bndle);
} catch (SchedulerException se) {。。。}
// 这里就是将JobRunShell交给线程池的地方
if (qsRsrcs.getThreadPool().runInThread(shell) == false) {。。。}
。。。
}
}
}
}
跟踪JobStore的acquireNextTrigger方法(这里是RAMJobStore)
// 实际上RAMJobStore持有一个TreeSet<Trigger> timeTriggers,排序方式是按触发时间排的。触发时间越早的排在前面。
// 所以这里只要取timeTriggers的first并验证就可以了。
public Trigger acquireNextTrigger(SchedulingContext ctxt, long noLaterThan) {
TriggerWrapper tw = null;
synchronized (lock) {
while (tw == null) {
try {
tw = (TriggerWrapper) timeTriggers.first();
} catch (java.util.NoSuchElementException nsee) {
return null;
}
if (tw == null) {
return null;
}
if (tw.trigger.getNextFireTime() == null) {
timeTriggers.remove(tw);
tw = null;
continue;
}
timeTriggers.remove(tw);
if (applyMisfire(tw)) {
if (tw.trigger.getNextFireTime() != null) {
timeTriggers.add(tw);
}
tw = null;
continue;
}
if(tw.trigger.getNextFireTime().getTime() > noLaterThan) {
timeTriggers.add(tw);
return null;
}
tw.state = TriggerWrapper.STATE_ACQUIRED;
tw.trigger.setFireInstanceId(getFiredTriggerRecordId());
Trigger trig = (Trigger) tw.trigger.clone();
return trig;
}
}
return null;
}
这里还有一点,Trigger是怎么知道自己的触发时间的。这里使用的是CronTrigger。通过源码可以知道,Trigger的下次触发时间是通过getNextFireTime方法得到的。CronTrigger的getNextFireTime方法是通过CronExpression对象的getTimeAfter方法实现的。CronExpression对象就是表示我们配置的触发表达式的对象。类似这样:0 0/10 * * * *
计算方法:
CronTrigger.getTimeAfter() 方法内部会调用CronExpression.getTimeAfter()方法。。。。。
利用Calendar类,单独设置年月日小时分秒的值。
年月日小时分秒都有一个TreeSet存储可能出现的所有的值,然后取当前时间之后的部分的第一个。就是下次触发的值。
PS:之前还以为多复杂,看了源码之后才知道,我们都被忽悠了。
难怪CronTrigger的触发表达式要这样写: 0 0/10 * * * 。。。
最终,JobRunShell就这样被启动了。
最后再回到 SchedulerFactoryBean 的start方法:
public void start() throws SchedulingException {
if (this.scheduler != null) {
try {
startScheduler(this.scheduler, this.startupDelay);
}
catch (SchedulerException ex) {
throw new SchedulingException("Could not start Quartz Scheduler", ex);
}
startScheduler中会调用前面创建的scheduler对象的start方法。将Quartz的信号量置为false,启动Quartz主线程
public void start() throws SchedulerException {
if (shuttingDown|| closed) {
throw new SchedulerException(
"The Scheduler cannot be restarted after shutdown() has been called.");
}
if (initialStart == null) {
initialStart = new Date();
this.resources.getJobStore().schedulerStarted();
startPlugins();
}
// 这里就是将主线程的pause信号量置为false的地方
schedThread.togglePause(false);
getLog().info(
"Scheduler " + resources.getUniqueIdentifier() + " started.");
notifySchedulerListenersStarted();
}
Quartz的工作原理和源码分析大概就是这样,知道了原理并不是很复杂的。
再回到项目中,之前那个操蛋的问题到底出在哪呢?一路分析下来我发现都没问题,Quartz是正常启动了。原因就在于有个同事提交了代码,没通知我,本地的代码与服务器上的代码已经不一致了,我还傻乎乎的远程断点调试,当然看不到进断点了。