上一篇文章中,我們分析了Quartz框架的Job
和Trigger
的源碼實現,上篇也說到,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
調用的就是SimpleThreadPool
的runInThread
方法。
通過源碼分析,我們很明顯能感覺到,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基礎/工具使用的文章,看來要搞一些新手教程向的文章,提高提高博客的點擊率了。
最後,希望大家都活成一個軟件開發工程師,而不是一個碼農。保持好奇,保持學習。