Quartz源码分析(下)

上一篇文章中,我们分析了Quartz框架的JobTrigger的源码实现,上篇也说到,Quartz的核心代码是Scheduler,在本篇中,我们会分析一下Scheduler的源码实现。
在这里插入图片描述

1、核心入口类:QuartzScheduler

现在我们有了包装好的JobDetail对象,有了具有时间表规则的Trigger对象,只需要将这两个对象提交给Scheduler就可以让Quartz框架按照我们定义的时间规则执行Job了。

其实到这里,我们已经发现,无论是Job还是Tigger以及他们的各种Builder,都没有真正的执行逻辑,相当于沾板(配菜切菜)在处理食材,真正开始烹饪还是得靠大厨—Scheduler

Scheduler采用工厂模式创建,这里我们直接看Quartz中的核心实现类QuartzScheduler,这个类可以说是Quartz的心脏,话不多说,我们先看这个类的介绍:

* <p>
* This is the heart of Quartz, an indirect implementation of the
*  <code>{@link org.quartz.Scheduler}</code>
* interface, containing methods to schedule 
* <code>{@link org.quartz.Job}</code>s,
* register <code>{@link org.quartz.JobListener}</code> instances, etc.
* </p>

这个类是Scheduler的一个实现类,具有调度Job注册到JobListener实例的方法。

这个类中包含两个重要的字段,如下:

// Scheduler需要的资源类,包括工作线程池和JobStore存储Job和Trigger映射关系
private QuartzSchedulerResources resources;

// Shceduler调度线程,这个线程负责根据Trigger的出发时间触发Job执行
private QuartzSchedulerThread schedThread;

这两个类也是很重要功能组成部分,QuartzSchedulerResources类是一个资源类,从类名也能看出来,这里先大致了解它的作用,这个类里面有工作线程需要用的线程池,以及上一篇中我们提到的JobStore对象。

其次就是QuartzSchedulerThread类,这个类是调度任务的核心类,整体Quartz框架来看,是由一条单独的线程来进行任务的调度,就像我一开始想的,一条线程不断轮询来判断各个Trigger触发时间,然后找到Trigger对应的Job去运行。

2、调度线程类:QuartzSchedulerThread

上面说到了QuartzSchedulerThread类,我们先来看一下QuartzScheduler的构造方法:
在这里插入图片描述
可以看到,在构造函数调用时,就已经启动了QuartzSchedulerThread调度线程,相当于此时刚创建了Scheduler对象,还没有任何任务提交过来,这个线程已经启动了。

QuartzSchedulerThread的核心是run()方法,QuartzSchedulerThread比我想象中实现的更为直接,并没有任务队列,而是任务直接提交到可用的工作线程上,这也是为了任务的实时性(想想我那种想法也不太行,比较人家是定时任务,万一在队列里等了半天没执行,那不就完全违背了定时任务的设计理念了吗)

我们抽出run()方法中核心代码进行分析。

首先说明一点,run()方法是一个持续的while循环,直到这个Scheduler关闭,这样才能满足QuartzSchedulerThread作为一个调度线程的功能。下面看一段重要源码:

// 获得可用的worker线程数量
int availThreadCount = qsRsrcs.getThreadPool().blockForAvailableThreads();
// 可用的worker线程数量大于0时,才提交任务
if(availThreadCount > 0) { // will always be true, due to semantics of blockForAvailableThreads...

    List<OperableTrigger> triggers;
	// 当前系统时间
    long now = System.currentTimeMillis();

    clearSignaledSchedulingChange();
    try {	
		// 这里是核心代码,acquireNextTriggers()方法会返回将要触发的Trigger
		// 这里根据一个将来短暂的时间段来判断,返回在这个时间段内会触发的Trigger列表
        triggers = qsRsrcs.getJobStore().acquireNextTriggers(
                now + idleWaitTime, Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()), qsRsrcs.getBatchTimeWindow());
        acquiresFailed = 0;
    } catch (JobPersistenceException jpe) {
	// 省略代码…
        continue;
    } catch (RuntimeException e) {
	// 省略代码…
        continue;
    }

这里Quartz的机制是,JobStore对象中拿到下一组要触发的Tigger对象,也就是说拿到未来一个小时间段内将要触发的任务,而不是只拿到现在需要执行的任务。

我们知道,目前拿到的Trigger列表只是未来一段时间内要执行的Trigger,他们的触发时机必定是一个特定的时间点

从逻辑上分析一下,这里必然要拿到未来最早要触发的任务时间,才是最合理的。

那继续分析下一段关键代码,看Quartz是怎么处理的。

if (triggers != null && !triggers.isEmpty()) {
	// 获取当前系统时间
    now = System.currentTimeMillis(); 
    
    // 获取最近要触发的任务的触发时间
    long triggerTime = triggers.get(0).getNextFireTime().getTime(); 
    long timeUntilTrigger = triggerTime - now; // 获取触发倒计时
    while(timeUntilTrigger > 2) { 
	// 这里判断依据是2毫秒,其实是因为线程休眠唤醒也是需要一定时间的 
	// 因此如果触发倒计时小于2毫秒 ,就认为这时已经可以触发任务了
        synchronized (sigLock) {
            if (halted.get()) {
                break;
            }
	
			// 这里是判断是否需值得休眠,因为有可能存在Scheduler被暂停(paused)
			//  被终止(halt)的情况,这时候就没必要休眠了
            if (!isCandidateNewTimeEarlierWithinReason(triggerTime, false)) {
                try {
                    // we could have blocked a long while
                    // on 'synchronize', so we must recompute
                    now = System.currentTimeMillis();
                    timeUntilTrigger = triggerTime - now;
                    if(timeUntilTrigger >= 1)
					// 这里是让当前线程等待,等待时间就是触发倒计时
					// 让线程等待的原因是防止持续while循环导致cpu占用过高
                        sigLock.wait(timeUntilTrigger);
                } catch (InterruptedException ignore) {
                }
            }
        }
        if(releaseIfScheduleChangedSignificantly(triggers, triggerTime)) {
            break;
        }
        now = System.currentTimeMillis();
        timeUntilTrigger = triggerTime - now;
    }

可以看到,这里其实就是等待到达下一个触发时间点,等待的时间段内,Quartz让线程wait指定时间,被唤醒以后继续向下运行,被唤醒的时候相当于到达了Trigger触发时间点(可能会有毫秒级别的误差)。

唤醒以后的线程,此时到达了Trigger触发的时间点,下一步很明显就是要执行Trigger关联的任务了。

List<TriggerFiredResult> bndles = new ArrayList<TriggerFiredResult>();

boolean goAhead = true;
synchronized(sigLock) {
	// 再次判断任务是否已经取消
    goAhead = !halted.get();
}
if(goAhead) { 
    try {
		// 这里获得的TriggerFiredResult是一个包装过的JobDetail和Trigger对象
		// 也就是找到了Trigger关联的Job
        List<TriggerFiredResult> res = qsRsrcs.getJobStore().triggersFired(triggers);
        if(res != null)
            bndles = res;
    } catch (SchedulerException se) {
        qs.notifySchedulerListenersError(
                "An error occurred while firing triggers '"
                        + triggers + "'", se);
        //QTZ-179 : a problem occurred interacting with the triggers from the db
        //we release them and loop again
        for (int i = 0; i < triggers.size(); i++) {
			// 出现异常,就释放这些准备执行的Trigger,重新放回等待列表
            qsRsrcs.getJobStore().releaseAcquiredTrigger(triggers.get(i));
        }
        continue;
    }

}

现在我们已经拿到了当前时间点需要触发的Trigger以及其关联的JobDetail,下一步就是把Job提交给Quartz的WorkerThreadPool(工作线程池),肯定不能在Scheduler的线程里去执行任务,因为Scheduler线程只是作为一个时间调度线程,需要保证实时性而且不能被阻塞,因此任务工作线程就必然是额外的线程。

for (int i = 0; i < bndles.size(); i++) {
    TriggerFiredResult result =  bndles.get(i);
    TriggerFiredBundle bndle =  result.getTriggerFiredBundle();
    Exception exception = result.getException();
	/*
	省略代码……
	*/
    if (bndle == null) {
        qsRsrcs.getJobStore().releaseAcquiredTrigger(triggers.get(i));
        continue;
    }

    JobRunShell shell = null;
    try {
		// 这里就是包装Job成为一个shell
        shell = qsRsrcs.getJobRunShellFactory().createJobRunShell(bndle);
        shell.initialize(qs);
    } catch (SchedulerException se) {
       	// 省略代码……
        continue;
    }

    // 核心代码:提交job到Worker线程池执行任务
    if (qsRsrcs.getThreadPool().runInThread(shell) == false) {
        getLog().error("ThreadPool.runInThread() return false!");
        qsRsrcs.getJobStore().
        		triggeredJobComplete(triggers.get(i), bndle.getJobDetail(), 
        CompletedExecutionInstruction.SET_ALL_JOB_TRIGGERS_ERROR);
    }

}

到这里,Scheduler作为时间调度器的代码实际上已经完毕,下面就是WorkerThread去执行任务的代码了。

3、任务执行线程池:SimpleThreadPool

Quartz框架默认实现了一个简单线程池SimpleThreadPool类,也就是说Worker线程池是Quartz框架实现的,而不是直接使用的Java线程池(因为要满足任务执行的时效性,Java线程池设计上有一种异步的理念,不太适合Quartz使用)

先来看一下SimpleThreadPool类结构图,其中有一个内部类WorkerThread,是一个继承自Java.lang.Thread的类,当然Quartz为了实现其工作线程的功能,重写了run()方法,这个我们一会再进行分析,SimpleThreadPool类其他一些属性就是用来标志一些线程池的属性。

在这里插入图片描述

我们来分析一下SimpleThreadPool的原理,首先来看其初始化方法:

public void initialize() throws SchedulerConfigException {
	/*
	省略部分代码,主要是错误处理的代码……
	*/

	// 这里count是从配置文件中得到的线程数量,默认是10个工作线程,会创建10个线程
    Iterator<WorkerThread> workerThreads = createWorkerThreads(count).iterator();
    while(workerThreads.hasNext()) {
        WorkerThread wt = workerThreads.next();
        wt.start(); // 直接启动线程
        availWorkers.add(wt); // 添加到线程池的可用线程列表中
    }
}

可以看到,工作线程池的初始化方法很容易理解,就是循环创建了多个工作线程并启动他们,这里有个细节,就是工作线程池是在SchedulerFactory工厂创建Scheduler对象的时候就已经创建了,也就是说还早于调度线程QuartzSchedulerThread的创建。

下面我们分析一下SimpleThreadPool线程池中执行任务时调用的runInThread方法。

public boolean runInThread(Runnable runnable) {
    if (runnable == null) {
        return false;
    }

	// 这里单纯是为了wait使用,没有并发安全问题
    synchronized (nextRunnableLock) {

        handoffPending = true;

        // Wait until a worker thread is available
		// 如果当前没有空闲工作线程,就等待500毫秒
        while ((availWorkers.size() < 1) && !isShutdown) {
            try {
                nextRunnableLock.wait(500);
            } catch (InterruptedException ignore) {
            }
        }

        if (!isShutdown) {
			// 这里是正常运行任务的流程,同时把线程从空闲线程队列移动到工作线程队列
            WorkerThread wt = (WorkerThread)availWorkers.removeFirst();
            busyWorkers.add(wt);
            wt.run(runnable);  // 执行任务
        } else {
            // If the thread pool is going down, execute the Runnable
            // within a new additional worker thread (no thread from the pool).
			// 这里是一种补救机制,如果线程池已经关闭,但是此时任务已经被提交
			// 就单独开一个线程来执行任务(此时其他工作线程可能 正在/已经 销毁)
            WorkerThread wt = new WorkerThread(this, threadGroup,
                    "WorkerThread-LastJob", prio, isMakeThreadsDaemons(), runnable);
            busyWorkers.add(wt);
            workers.add(wt);
            wt.start();
        }
        nextRunnableLock.notifyAll();
        handoffPending = false;
    }

    return true;
}

在上面QuartzSchedulerThread调用的就是SimpleThreadPoolrunInThread方法。

通过源码分析,我们很明显能感觉到,SimpleThreadPool中的runInTread方法,其实非常像Java线程池中的execute()方法,都是提交一个任务到线程池中,只不过SimpleThreadPool会保证这个任务会被立即执行(如果不能立即执行就等待到执行为止),而不是像Java线程池会提交到一个等待队列,让线程池慢慢消化。这也体现了Quartz框架的特点,就是任务执行的时效性。

4、任务执行线程类:WorkerThread

来看一下WorkerThread类的内部结构,可以看到有两个run()方法,我们知道实现Runnable接口需要实现run()方法,这里它还有一个run(Runnable)方法,其实这个方法名起的很差,这个方法本质上是 提交/执行 一个任务,不如改为submit() / execute() 更让人清楚。

在这里插入图片描述

我们来看一下SimpleThreadPool.runInThread使用的run(Runnable)方法,其实就是把WorkerThread需要执行的任务提交进来。

public void run(Runnable newRunnable) {
    synchronized(lock) {
        if(runnable != null) {
            throw new IllegalStateException("Already running a Runnable!");
        }

		// runnable是类属性
        runnable = newRunnable;
        lock.notifyAll(); // 主动唤醒处于等待状态的线程
    }
}

那么继承Thread类重写的run()方法又是干什么的?很明显这个应该是工作线程真实工作时的方法。

     * <p>
     * Loop, executing targets as they are received.
     * 意思很简答:循环,执行接收到的任务
     * </p>
     */
    @Override
    public void run() {
        boolean ran = false;
        // 这里时判断是否要关闭线程池,如果关闭就退出循环,销毁线程
        while (run.get()) {
            try {
                synchronized(lock) {
					// 如果没有任务需要执行,就进入等待状态
					// 这样做的原因是减少空循环带来的cpu资源销毁
                    while (runnable == null && run.get()) {
                        lock.wait(500); // 等待500ms
                    }

                    if (runnable != null) {
                        ran = true;
                        runnable.run(); // 执行提交的任务
                    }
                }
            } catch (InterruptedException unblock) {
                // do nothing (loop will terminate if shutdown() was called
                try {
                    getLog().error("Worker thread was interrupt()'ed.", unblock);
                } catch(Exception e) {
                    // ignore to help with a tomcat glitch
                }
            } catch (Throwable exceptionInRunnable) {
                try {
                    getLog().error("Error while executing the Runnable: ",
                        exceptionInRunnable);
                } catch(Exception e) {
                    // ignore to help with a tomcat glitch
                }
            } finally {
                synchronized(lock) {
                    runnable = null;
                }
                // repair the thread in case the runnable mucked it up...
                if(getPriority() != tp.getThreadPriority()) {
                    setPriority(tp.getThreadPriority());
                }
                // 这里是我上面说的补救情况,就是线程池处于关闭情况,此时一个任务被提交进来
				// 就单独启动一个线程完成提交的任务,执行完成以后线程销毁
                if (runOnce) {
                       run.set(false);
                    clearFromBusyWorkersList(this);
                } else if(ran) {
                    ran = false;
                    makeAvailable(this); // 从工作队列中重新放入空闲队列
                }

            }
        }

        //if (log.isDebugEnabled())
        try {
            getLog().debug("WorkerThread is shut down.");
        } catch(Exception e) {
            // ignore to help with a tomcat glitch
        }
    }
}

可以看到这个工作线程类的是实现方法,比起Java线程池来说简单了不知道多少倍,就是不断地循环,等待任务传入,然后执行,从线程池的角度来看,是一个同步阻塞的模型。

5、总结:

到这里为止,我们分析完了Quartz的核心执行任务的流程,其中一些思想还是值得学习的,比如说线程模型,比如说任务调度算法。

当然,Quartz中还有很多代码,比如说Cron表达式的解析转换逻辑,比如说数据库存储分布式部署方式等等很多代码,我们就不一一分析了,有兴趣的小伙伴可以自己去查看。

当然,源码分析中可能存在一些错误,因为这些都是我自己阅读的,当然其中还有一些没看懂的部分,希望大家能批评指正。

源码分析类的文章太费时间了,而且从点击率来看,远远低于一些Java基础/工具使用的文章,看来要搞一些新手教程向的文章,提高提高博客的点击率了。

最后,希望大家都活成一个软件开发工程师,而不是一个码农。保持好奇,保持学习。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章