资源
官网:<http://www.quartz-scheduler.org/>
<https://zhuanlan.zhihu.com/p/128567942>
<https://www.jianshu.com/p/e634b135df90>
什么是 Quartz ?
Quartz是一个Java版开源任务调度框架,功能强悍,使用方便。
Quartz 核心概念
1、Job
表示一个工作,要执行的具体内容。此接口中只有一个方法 void execute(JobExecutionContext context)
2、JobDetail
JobDetail表示一个具体的可执行的调度程序,Job是这个可执行程调度程序所要执行的内容,另外JobDetail还包含了这个任务调度的方案和策略
3、Trigger
代表一个调度参数的配置,什么时候去调
4、Scheduler
代表一个调度容器,一个调度容器中可以注册多个JobDetail和Trigger。当Trigger与JobDetail组合,就可以被Scheduler容器调度了
元素之间的关系
先由SchedulerFactory创建Scheduler调度器后,由调度器去调取即将执行的Trigger,执行时获取到对应的JobDetail信息,找到对应的Job类执行业务逻辑。
代码示例:
public void addJob(QuartzJobBo job){
logger.info("添加定时任务: " + JacksonUtils.toJson(job));
try {
// 创建任务对象实例,绑定Job实现类,指明任务对象的名称,所在组的名称,以及绑定job类
Class<? extends Job> jobClass = (Class<? extends Job>) (Class.forName(job.getExecuteClassName()).newInstance().getClass());
// 任务名称和组 构成任务 key
JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(job.getJobName(),job.getJobGroup()).build();
// 定义调度触发规则,使用cornTrigger规则,触发器key
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(job.getJobName(),job.getJobGroup())
// 指定触发器生效的时间
.startAt(DateBuilder.futureDate(1, DateBuilder.IntervalUnit.SECOND))
// 指定触发器规则
.withSchedule(CronScheduleBuilder.cronSchedule(job.getCron()))
.startNow().build();
// 把任务和触发器注册到任务调度中
scheduler.scheduleJob(jobDetail, trigger);
// 启动
if (!scheduler.isShutdown()) {
scheduler.start();
}
} catch (Exception e) {
logger.error("添加定时任务异常: " + e.getMessage(), e);
}
}
底层原理介绍
quartz中线程主要分为执行线程和调度线程
在quartz中,Scheduler调度线程主要有两个:regular scheduler thread (执行常规调度) 和 misfire scheduler thread (执行错失的任务)。其中 regular thread 轮询 trigger,如果有将要触发的 trigger,则从执行线程池中获取一个空闲线程,然后执行与改 trigger 关联的 job;misfire thraed 则是扫描所有的 trigger,查看是否有错失的,如果有的话,根据一定的策略进行处理。
创建 Scheduler
StdSchedulerFactory.getScheduler()源码
public Scheduler getScheduler() throws SchedulerException {
// 读取quartz配置文件,未指定则顺序遍历各个path下的quartz.properties文件
// 解析出quartz配置内容和环境变量,存入PropertiesParser对象
// PropertiesParser组合了Properties(继承Hashtable),定义了一系列对Properties的操作方法,比如getPropertyGroup()批量获取相同前缀的配置。配置内容和环境变量存放在Properties成员变量中
if (cfg == null) {
initialize();
}
// 获取调度器池,采用了单例模式
// 其实,调度器池的核心变量就是一个hashmap,每个元素key是scheduler名,value是scheduler实例
// getInstance()用synchronized防止并发创建
SchedulerRepository schedRep = SchedulerRepository.getInstance();
// 从调度器池中取出当前配置所用的调度器
Scheduler sched = schedRep.lookup(getSchedulerName());
......
// 如果调度器池中没有当前配置的调度器,则实例化一个调度器,主要动作包括:
// 1)初始化threadPool(线程池):开发者可以通过org.quartz.threadPool.class配置指定使用哪个线程池类,比如SimpleThreadPool。先class load线程池类,接着动态生成线程池实例bean,然后通过反射,使用setXXX()方法将以org.quartz.threadPool开头的配置内容赋值给bean成员变量;
// 2)初始化jobStore(任务存储方式):开发者可以通过org.quartz.jobStore.class配置指定使用哪个任务存储类,比如RAMJobStore。先class load任务存储类,接着动态生成实例bean,然后通过反射,使用setXXX()方法将以org.quartz.jobStore开头的配置内容赋值给bean成员变量;
// 3)初始化dataSource(数据源):开发者可以通过org.quartz.dataSource配置指定数据源详情,比如哪个数据库、账号、密码等。jobStore要指定为JDBCJobStore,dataSource才会有效;
// 4)初始化其他配置:包括SchedulerPlugins、JobListeners、TriggerListeners等;
// 5)初始化threadExecutor(线程执行器):默认为DefaultThreadExecutor;
// 6)创建工作线程:根据配置创建N个工作thread,执行start()启动thread,并将N个thread顺序add进threadPool实例的空闲线程列表availWorkers中;
// 7)创建调度器线程:创建QuartzSchedulerThread实例,并通过threadExecutor.execute(实例)启动调度器线程;
// 8)创建调度器:创建StdScheduler实例,将上面所有配置和引用组合进实例中,并将实例存入调度器池中
ched = instantiate();
return sched;
QuartzScheduler.scheduleJob(JobDetail, Trigger)源码
public Date scheduleJob(JobDetail jobDetail,
Trigger trigger) throws SchedulerException {
// 检查调度器是否开启,如果关闭则throw异常到上层
validateState();
......
// 获取trigger首次触发job的时间,以此时间为起点,每隔一段指定的时间触发job
Date ft = trig.computeFirstFireTime(cal);
if (ft == null) {
throw new SchedulerException(
"Based on configured schedule, the given trigger '" + trigger.getKey() + "' will never fire.");
}
// 把job和trigger注册进调度器的jobStore
resources.getJobStore().storeJobAndTrigger(jobDetail, trig);
// 通知job监听者
notifySchedulerListenersJobAdded(jobDetail);
// 通知调度器线程
notifySchedulerThread(trigger.getNextFireTime().getTime());
// 通知trigger监听者
notifySchedulerListenersSchduled(trigger);
return ft;
}
QuartzScheduler.start()源码
public void start() throws SchedulerException {
......
// 这句最关键,作用是使调度器线程跳出一个无限循环,开始轮询所有trigger触发job
// 原理详见“如何采用多线程进行任务调度”
schedThread.togglePause(false);
......
}
如何采用多线程进行任务调度
// 调度器线程一旦启动,将一直运行此方法
public void run() {
......
// while()无限循环,每次循环取出时间将到的trigger,触发对应的job,直到调度器线程被关闭
// halted是一个AtomicBoolean类变量,有个volatile int变量value,其get()方法仅仅简单的一句return value != 0,get()返回结果表示调度器线程是否开关
// volatile修饰的变量,存取必须走内存,不能通过cpu缓存,这样一来get总能获得set的最新真实值,因此volatile变量适合用来存放简单的状态信息
// 顾名思义,AtomicBoolean要解决原子性问题,但volatile并不能保证原子性,详见http://blog.csdn.net/wxwzy738/article/details/43238089
while (!halted.get()) {
try {
// check if we're supposed to pause...
// sigLock是个Object对象,被用于加锁同步
// 需要用到wait(),必须加到synchronized块内
synchronized (sigLock) {
while (paused && !halted.get()) {
try {
// wait until togglePause(false) is called...
// 这里会不断循环等待,直到QuartzScheduler.start()调用了togglePause(false)
// 调用wait(),调度器线程进入休眠状态,同时sigLock锁被释放
// togglePause(false)获得sigLock锁,将paused置为false,使调度器线程能够退出此循环,同时执行sigLock.notifyAll()唤醒调度器线程
sigLock.wait(1000L);
} catch (InterruptedException ignore) {}
}
......
}
......
// 如果线程池中的工作线程个数 > 0
if(availThreadCount > 0) {
......
// 获取马上到时间的trigger
// 允许取出的trigger个数不能超过一个阀值,这个阀值是线程池个数与org.quartz.scheduler.batchTriggerAcquisitionMaxCount配置值间的最小者
triggers = qsRsrcs.getJobStore().acquireNextTriggers(
now + idleWaitTime, Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()), qsRsrcs.getBatchTimeWindow());
......
// 执行与trigger绑定的job
// shell是JobRunShell对象,实现了Runnable接口
// SimpleThreadPool.runInThread(Runnable)从线程池空闲列表中取出一个工作线程
// 工作线程执行WorkerThread.run(Runnable),详见下方WorkerThread的讲解
if (qsRsrcs.getThreadPool().runInThread(shell) == false) { ...... }
} else {......}
......
} catch(RuntimeException re) {......}
} // while (!halted)
......
}
WorkerThread.java
public void run(Runnable newRunnable) {
synchronized(lock) {
if(runnable != null) {
throw new IllegalStateException("Already running a Runnable!");
}
runnable = newRunnable;
lock.notifyAll();
}
}
// 工作线程一旦启动,将一直运行此方法
@Override
public void run() {
boolean ran = false;
// 工作线程一直循环等待job,直到线程被关闭,原理同QuartzSchedulerThread.run()中的halted.get()
while (run.get()) {
try {
// 原理同QuartzSchedulerThread.run()中的synchronized (sigLock)
// 锁住lock,不断循环等待job,当job要被执行时,WorkerThread.run(Runnable)被调用,job运行环境被赋值给runnable
synchronized(lock) {
while (runnable == null && run.get()) {
lock.wait(500);
}
// 开始执行job
if (runnable != null) {
ran = true;
// runnable.run()将触发运行job实现类(比如JobImpl.execute())
runnable.run();
}
}
} catch (InterruptedException unblock) {
......
}
}
......
}
如何避免GC
Quartz里提供了一种方案,用来避免某些对象被GC。方案其实简单而实用,就是QuartzScheduler类
创建了一个列表ArrayList<Object>(5) holdToPreventGC
,如果某对象被add进该列表,则意味着QuartzScheduler
实例引用了此对象,那么此对象至少在QuartzScheduler
实例存活时不会被GC。
哪些对象要避免GC?通过源码可看到,调度器池和db管理器对象被放入了holdToPreventGC
,但实际上两种对象是static的,而static对象属于GC root,应该是不会被GC的,所以即使不放入holdToPreventGC
,这两种对象也不会被GC,除非被class unload或jvm生命结束。