Quartz - SimpleThreadPool

今天开始学习企业级作业调度框架Quartz。

先简单说明一下,Quartz的使用说明文档可以从官网获取:Quartz tutorials,而且中文文档也比较全面、也不难找到。如果想要练习或者学习如何使用Quartz,参考这类文档很容易上手。

我们会从另外一个角度学习并记录Quartz的学习过程:尽可能从源码的角度,了解Quartz的底层原理。

作为企业级作业调度框架,复杂程度当然和JDK Timer不在一个数量级,对Quartz的学习也不太容易一蹴而就,所以,做好准备,一步一步来。

一个Quartz的例子

尽管Quartz的底层原理比较复杂,但是使用起来也不算复杂。

第一步:创建Job对象,实现execute方法。
第二步:创建JobDetail。
第三步:创建Trigger。
第四步:创建Scheduler,将JobDetail对象和Trigger对象绑定到Scheduler中,启动Schedule

@Slf4j
public class HelloJob implements Job {
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
      log.info("I dont know want should i do ..."+Thread.currentThread().getId());
    }

    public static void main(String[] args) {
        JobDetail jobDetail = newJob(HelloJob.class)
                .withDescription("This is my first quartz job")
                .withIdentity("MyJob")
                .build();
        Trigger trigger = newTrigger()
                .withIdentity("myTriggger","MyGroup")
                .startNow()
                .withSchedule(simpleSchedule()
                        .withIntervalInSeconds(1)
                        .repeatForever())
                .build();

        try {
            Scheduler sche = new StdSchedulerFactory().getScheduler();
            sche.scheduleJob(jobDetail,trigger);
            sche.start();
        }catch(Exception e){
            e.printStackTrace();
        }

    }

运行结果:任务立即开始执行,每秒钟运行一次,符合预期。

从执行结果还可以发现,前10次任务每次运行时的线程id都不一样,第11次开始,以后的任务重复前10次的线程id。

Quartz包含的主要对象

Quartz包括的关键组件:

  1. Job:任务接口
  2. JobDetail:任务详情接口
  3. Trigger:触发器
  4. Schedule:任务调度器
  5. ScheduleThread:任务调度线程
  6. SimpleThreadPool:任务执行线程池
  7. WorkerThread:任务执行线程
  8. Job Store:任务存储

Job&JobDetail

Job是任务接口,包含一个execute方法。Job与JDK Timer中的TimerTask类似,是提供给应用实现任务逻辑的API。

Quartz还提供了一个JobDetail接口,最终绑定到任务调度器中的不是Job而是JobDetail,稍后我们会简单分析下这么做的原因。

JobDetail不需要应用去实现,Quartz提供了一个实现类JobDetailImpl,应用通过JoBuilder创建出来的JobDetail其实就是这个JobDetailImpl,JobDetailImpl会持有应用实现的Job实现类的类名,而不是直接持有Job实现类的对象。

Trigger

触发器,有点类似JDK Timer中的Timer对象,但又不完全一样,为了更大的灵活性,其实Quartz相当于把JDK Timer中的Timer对象分拆成Trigger和Schedule两个对象。

Triggle是负责设置任务触发规则的,有两个最基本的实现SimpleTriggleImpl和CronTriggerImpl,SimpleTriggleImpl实现比较简单的触发规则,CronTriggerImpl可以支持cron表达式,所以可以设置比较复杂的任务触发规则。

Schedule

Schedule是任务调度器,这部分应该是Quartz中比较复杂的部分。

任务调度器通过StdSchedulerFactory创建,默认实现是StdScheduler。

StdScheduler是代理模式的设计,将所有方法调用委托给他的属性QuartzScheduler,最终其实是由QuartzScheduler负责任务调度的。

我们从上面的例子代码中其实可以发现,JobDetail和Trigger都绑定到Schedule中了,其实也就是绑定在QuartzScheduler中,通过QuartzSchedulerResources属性持有。

QuartzScheduler还有一个重要的属性是QuartzSchedulerThread,他是真正的任务调度器,在QuartzScheduler初始化的过程中创建。

下面我们要说一下这个QuartzSchedulerThread。

QuartzSchedulerThread

QuartzSchedulerThread是Quartz的任务调度线程,其实Quartz受欢迎的一个关键原因之一就是他的线程管理机制,Quartz的任务调度线程和任务执行线程是分开的,这样他就可以避免JDK Timer的一个缺陷:任务执行会影响到任务的调度。

QuartzSchedulerThread在scheduleJob的同时会被启动,也就是我们上面代码中的sche.scheduleJob(jobDetail,trigger)调用完成后,QuartzSchedulerThread线程就被启动了,调度线程启动后就开始循环等待任务被触发器触发,这部分的代码逻辑我们后面会详细分析。

今天的主角是SimpleThreadPool,到现在他还没有出现呢,我们要把他引出来。

上面例子中创建Schedule的代码:

Scheduler sche = new StdSchedulerFactory().getScheduler();

会调用到StdSchedulerFactory的instantiate()方法,这个方法特别特别特别长...我们暂时不研究它,我们只关注和ThreadPool有关的部分。

正是在这个冗长的方法中会创建ThreadPool,默认是SimpleThreadPool,之后会按照配置完成ThreadPool的初始化,并将准备好的SimpleThreadPool送给QuartzSchedulerResources持有。

 tp.initialize();

然后在任务调度线程QuartzSchedulerThread的执行主体中(也就是他的run方法中),如果某一任务被触发,系统检查线程池是否有可用的线程:

int availThreadCount = qsRsrcs.getThreadPool().blockForAvailableThreads();

如果有可用的线程的话,系统继续各项检查获取到需要执行的任务后,将该任务交给线程池、线程池负责拿到一个可用线程并执行当前任务:

if (qsRsrcs.getThreadPool().runInThread(shell) == false) 

所以到现在我们已经大概知道了Quartz任务调度的简单逻辑,我们没有仔细研究任务触发的具体过程,原因是我们今天的主要任务是SimpleThreadPool。

好在我们现在已经知道了Quartz是在什么地方创建线程池、任务触发的时候是怎么把任务交给线程池的。我们就从这两个方面入手。

初识SimpleThreadPool

Quartz的SimpleThreadPool是ThreadPool接口的实现,顾名思义,是一个简单的线程池的实现,几个重要概念包括:

  1. count:只维护了一个线程数,没有最大线程数、最大活动线程数等概念。
  2. workers:工作线程,初始化的时候会创建count个线程并放在workers中。
  3. availWorkers:可用线程,workers中没有被调度的线程,放置在availWorkers中。
  4. busyWorkers:当应用需要一个线程去执行任务的时候,从availWorkers中获取到一个可用线程(从availWorkers中移出)后放置到busyWorkers中。

SimpleThreadPool初始化

通过initialize()方法完成初始化:

调用createWorkerThreads创建count个WorkerThread线程对象放置在workers中。

逐个启动新创建的Thread,同时放置在availWorkers中。

count可以通过Quartz.properties文件设置,假设我们设置的线程数为10,则初始化完成之后,线程池中会有10个线程被启动,并且都在可用状态(都在availWorkers中)。

SimpleThreadPool执行任务

上面从源码中我们已经知道,SimpleThreadPool通过runInThread方法执行任务。

如果availWorkers中没有可用线程的话,挂起等待直到其他任务释放线程。

    while ((availWorkers.size() < 1) && !isShutdown) {
                try {
                    nextRunnableLock.wait(500);
                } catch (InterruptedException ignore) {
                }
            }

然后从availWorkers中获取一个线程,同时将获取到的线程放置到busyWorkers中,并用该线程执行任务。

SimpleThradPool执行任务

线程池availWorkers和busyWorkers中存储的其实是WorkerThread对象,所以线程池执行任务调用的其实是WorkerThread的run方法。

WorkerThread持有一个Runnable成员对象,其实就是我们需要交给线程池执行的任务,run方法中不断检查是否有任务交进来,如果没有的话就挂起等待。

如果发现有任务交进来,则调用该Runnable的run方法,这个时候其实就能调用到应用层Job对象的execute方法了,但是具体怎么调用到的我们还是放到后面分析。

任务执行完成后将WorkerThread的Runnable对象清空,然后将线程交还给线程池的availWorkers中,并且从busyWorkers中移出。

OK!

上一篇 JAVA定时任务 - JDK Timer

你可能感兴趣的:(Quartz - SimpleThreadPool)