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基礎/工具使用的文章,看來要搞一些新手教程向的文章,提高提高博客的點擊率了。

最後,希望大家都活成一個軟件開發工程師,而不是一個碼農。保持好奇,保持學習。

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