Quartz

4 阅读7分钟

资源

官网:<http://www.quartz-scheduler.org/>

<https://zhuanlan.zhihu.com/p/128567942>
<https://www.jianshu.com/p/e634b135df90>

什么是 Quartz ?

Quartz是一个Java版开源任务调度框架,功能强悍,使用方便。

Untitled

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;

Untitled

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生命结束。