随笔-21  评论-5  文章-9 

Quartz学习笔记

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是正常启动了。原因就在于有个同事提交了代码,没通知我,本地的代码与服务器上的代码已经不一致了,我还傻乎乎的远程断点调试,当然看不到进断点了。

posted on 2012-08-28 13:45 云轩阁 阅读(...) 评论(...) 编辑 收藏