「Java系列」quartz原理揭秘和源碼解讀

導(dǎo)語(yǔ):作為java領(lǐng)域最受歡迎的任務(wù)調(diào)度庫(kù)之一,quartz為開(kāi)發(fā)者提供了豐富的任務(wù)調(diào)度功能,比如讓某段程序在每天18:00準(zhǔn)時(shí)執(zhí)行。本文將通過(guò)demo和源碼,講解quartz如何使用、主要功能有哪些、原理是什么,并挑選幾段有用的源碼片段進(jìn)行解讀。

quartz logo

1、quartz簡(jiǎn)介

quartz,即石英的意思,隱喻如石英表般對(duì)時(shí)間的準(zhǔn)確把握。

quartz是一個(gè)由java編寫(xiě)的任務(wù)調(diào)度庫(kù),由OpenSymphony組織開(kāi)源出來(lái)。那么問(wèn)題來(lái)了,任務(wù)調(diào)度是個(gè)什么東西?舉個(gè)栗子,現(xiàn)在有N個(gè)任務(wù)(程序),要求在指定時(shí)間執(zhí)行,比如每周二3點(diǎn)執(zhí)行任務(wù)A、每天相隔5s執(zhí)行任務(wù)B等等,這種多任務(wù)擁有多種執(zhí)行策略就是任務(wù)調(diào)度。而quartz的核心作用,是使任務(wù)調(diào)度變得豐富、高效、安全,開(kāi)發(fā)者只需要調(diào)幾個(gè)quartz接口并做簡(jiǎn)單配置,即可實(shí)現(xiàn)上述需求。

quartz號(hào)稱(chēng)能夠同時(shí)對(duì)上萬(wàn)個(gè)任務(wù)進(jìn)行調(diào)度,擁有豐富的功能特性,包括任務(wù)調(diào)度、任務(wù)持久化、可集群化、插件等。目前最新版本是2.2.3,從github[1]上看,2.2.4已在開(kāi)發(fā)中。

quartz有競(jìng)品嗎?有,那就是java Timer。quartz相對(duì)于java Timer的優(yōu)勢(shì)包括任務(wù)持久化、配置更豐富、線(xiàn)程池等等,詳見(jiàn)官網(wǎng)[2]解釋,兩者在Spring上的用法可見(jiàn)這篇文章[^OpenSymphony Quartz和java Timer]。

接下來(lái),筆者將從一個(gè)簡(jiǎn)單的demo開(kāi)始,順著demo里使用到的quartz接口,逐個(gè)分析quartz主要功能及其原理。限于篇幅,demo中未涉及的功能,本文不涉及,比如集群化等。最后,挑選2段對(duì)大家日常開(kāi)發(fā)有用的源碼進(jìn)行解讀。

讀這篇文章有什么用

  • 對(duì)一個(gè)任務(wù)調(diào)度系統(tǒng)產(chǎn)生初步的原理級(jí)了解
  • 更正確地使用quartz
  • 學(xué)到如何采用多線(xiàn)程進(jìn)行任務(wù)調(diào)度的源碼
  • 學(xué)到如何避免GC的源碼
  • 精(xian)力(de)充(dan)沛(teng),隨便找篇技術(shù)文讀讀

[^OpenSymphony Quartz和java Timer]: yaerfengSpring定時(shí)器配置的兩種實(shí)現(xiàn)方式OpenSymphony Quartz和java Timer詳解

2、使用代碼demo[3]

本文的demo程序由2個(gè)java文件和quartz.properties組成,quartz.properties是可選的,因?yàn)閝uartz有默認(rèn)配置。demo實(shí)現(xiàn)從當(dāng)前時(shí)間開(kāi)始,每隔2s執(zhí)行一次JobImpl類(lèi)的execute()方法。

  • TestQuartz.java
/**
 * 從當(dāng)前時(shí)間開(kāi)始,每隔2s執(zhí)行一次JobImpl#execute()
 * @author hlx
 */
public class TestQuartz {
    public static void main(String[] args) throws SchedulerException, InterruptedException {
      // 創(chuàng)建調(diào)度器
      SchedulerFactory schedulerFactory = new StdSchedulerFactory();
      Scheduler scheduler = schedulerFactory.getScheduler();
  
      // 創(chuàng)建任務(wù)
      JobDetail jobDetail = JobBuilder.newJob(JobImpl.class).withIdentity("myJob", "jobGroup").build();
  
      // 創(chuàng)建觸發(fā)器
      // withIntervalInSeconds(2)表示每隔2s執(zhí)行任務(wù)
      Date triggerDate = new Date();
      SimpleScheduleBuilder schedBuilder = SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(2).repeatForever();
      TriggerBuilder<Trigger> triggerBuilder  = TriggerBuilder.newTrigger().withIdentity("myTrigger", "triggerGroup");
      Trigger trigger = triggerBuilder.startAt(triggerDate).withSchedule(schedBuilder).build();

      // 將任務(wù)及其觸發(fā)器放入調(diào)度器
      scheduler.scheduleJob(jobDetail, trigger);
      // 調(diào)度器開(kāi)始調(diào)度任務(wù)
      scheduler.start();
   }
}
  • JobImpl.java
/**
 * @author hlx
 */
public class JobImpl implements Job {
  public void execute(JobExecutionContext context) {
    System.out.println("job impl running");
  }
}
  • quartz.properties(可選)
#調(diào)度器名,默認(rèn)名是QuartzScheduler
org.quartz.scheduler.instanceName: TestQuartzScheduler

#============================================================================
# Configure ThreadPool   配置線(xiàn)程池
#============================================================================
org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount: 10
org.quartz.threadPool.threadPriority: 5

#============================================================================
# Configure JobStore  配置任務(wù)存儲(chǔ)方式
#============================================================================
#相當(dāng)于掃描頻率
org.quartz.jobStore.misfireThreshold: 60000
org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore

我們順著注釋看到,TestQuartz.main()依次創(chuàng)建了scheduler(調(diào)度器)、job(任務(wù))、trigger(觸發(fā)器),其中,job指定了JobImpl,trigger保存job的觸發(fā)執(zhí)行策略(每隔2s執(zhí)行一次),scheduler將job和trigger綁定在一起,最后scheduler.start()啟動(dòng)調(diào)度,每隔2s觸發(fā)執(zhí)行JobImpl.execute(),打印出job impl running

對(duì)于quartz.properties,簡(jiǎn)單場(chǎng)景下,開(kāi)發(fā)者不用自定義配置,使用quartz默認(rèn)配置即可,但在要求較高的使用場(chǎng)景中還是要自定義配置,比如通過(guò)org.quartz.threadPool.threadCount設(shè)置足夠的線(xiàn)程數(shù)可提高多job場(chǎng)景下的運(yùn)行性能。更詳盡的配置見(jiàn)官網(wǎng)配置說(shuō)明頁(yè)

如果讓我們?cè)O(shè)計(jì)一個(gè)任務(wù)調(diào)度系統(tǒng),會(huì)像quartz那樣將job、trigger、scheduler解藕?jiǎn)幔縬uartz這樣設(shè)計(jì)的原因,筆者認(rèn)為有兩點(diǎn):

  • job與trigger解藕,其實(shí)就是將任務(wù)本身和任務(wù)執(zhí)行策略解藕,這樣可以方便實(shí)現(xiàn)N個(gè)任務(wù)和M個(gè)執(zhí)行策略自由組合,比較容易理解;
  • scheduler單獨(dú)分離出來(lái),相當(dāng)于一個(gè)指揮官,可以從全局做調(diào)度,比如監(jiān)聽(tīng)哪些trigger已經(jīng)ready、分配線(xiàn)程等等,如果沒(méi)有scheduler,則trigger間會(huì)競(jìng)爭(zhēng)混亂,難以實(shí)現(xiàn)諸如trigger優(yōu)先級(jí)等功能,也無(wú)法合理使用資源。

下面,筆者將分別就jobtriggerscheduler進(jìn)行原理分析。

3、job(任務(wù))

job由若干個(gè)classinterface實(shí)現(xiàn)。

Job接口

開(kāi)發(fā)者想要job完成什么樣的功能,必須且只能由開(kāi)發(fā)者自己動(dòng)手來(lái)編寫(xiě)實(shí)現(xiàn),比如demo中的JobImpl,這點(diǎn)無(wú)容置疑。但要想讓自己的job被quartz識(shí)別,就必須按照quartz的規(guī)則來(lái)辦事,這個(gè)規(guī)則就是job實(shí)現(xiàn)類(lèi)必須實(shí)現(xiàn)Job接口,比如JobImpl就實(shí)現(xiàn)了Job

Job只有一個(gè)execute(JobExecutionContext)JobExecutionContext保存了job的上下文信息,比如綁定的是哪個(gè)trigger。job實(shí)現(xiàn)類(lèi)必須重寫(xiě)execute(),執(zhí)行job實(shí)際上就是運(yùn)行execute()

JobDetailImpl類(lèi) / JobDetail接口

JobDetailImpl類(lèi)實(shí)現(xiàn)了JobDetail接口,用來(lái)描述一個(gè)job,定義了job所有屬性及其get/set方法。了解job擁有哪些屬性,就能知道quartz能提供什么樣的能力,下面筆者用表格列出job若干核心屬性。

屬性名 說(shuō)明
class 必須是job實(shí)現(xiàn)類(lèi)(比如JobImpl),用來(lái)綁定一個(gè)具體job
name job名稱(chēng)。如果未指定,會(huì)自動(dòng)分配一個(gè)唯一名稱(chēng)。所有job都必須擁有一個(gè)唯一name,如果兩個(gè)job的name重復(fù),則只有最前面的job能被調(diào)度
group job所屬的組名
description job描述
durability 是否持久化。如果job設(shè)置為非持久,當(dāng)沒(méi)有活躍的trigger與之關(guān)聯(lián)的時(shí)候,job會(huì)自動(dòng)從scheduler中刪除。也就是說(shuō),非持久job的生命期是由trigger的存在與否決定的
shouldRecover 是否可恢復(fù)。如果job設(shè)置為可恢復(fù),一旦job執(zhí)行時(shí)scheduler發(fā)生hard shutdown(比如進(jìn)程崩潰或關(guān)機(jī)),當(dāng)scheduler重啟后,該job會(huì)被重新執(zhí)行
jobDataMap 除了上面常規(guī)屬性外,用戶(hù)可以把任意kv數(shù)據(jù)存入jobDataMap,實(shí)現(xiàn)job屬性的無(wú)限制擴(kuò)展,執(zhí)行job時(shí)可以使用這些屬性數(shù)據(jù)。此屬性的類(lèi)型是JobDataMap,實(shí)現(xiàn)了Serializable接口,可做跨平臺(tái)的序列化傳輸

JobBuilder類(lèi)

// 創(chuàng)建任務(wù)
JobDetail jobDetail = JobBuilder.newJob(JobImpl.class).withIdentity("myJob", "jobGroup").build();

上面代碼是demo一個(gè)片段,可以看出JobBuilder類(lèi)的作用:接收job實(shí)現(xiàn)類(lèi)JobImpl,生成JobDetail實(shí)例,默認(rèn)生成JobDetailImpl實(shí)例。

這里運(yùn)用了建造者模式:JobImpl相當(dāng)于Product;JobDetail相當(dāng)于Builder,擁有job的各種屬性及其get/set方法;JobBuilder相當(dāng)于Director,可為一個(gè)job組裝各種屬性。

4、trigger(觸發(fā)器)

trigger由若干個(gè)classinterface實(shí)現(xiàn)。

SimpleTriggerImpl類(lèi) / SimpleTrigger接口 / Trigger接口

SimpleTriggerImpl類(lèi)實(shí)現(xiàn)了SimpleTrigger接口SimpleTrigger接口繼承了Trigger接口,它們表示觸發(fā)器,用來(lái)保存觸發(fā)job的策略,比如每隔幾秒觸發(fā)job。實(shí)際上,quartz有兩大觸發(fā)器:SimpleTriggerCronTrigger,限于篇幅,本文僅介紹SimpleTrigger

Trigger諸類(lèi)保存了trigger所有屬性,同job屬性一樣,了解trigger屬性有助于我們了解quartz能提供什么樣的能力,下面筆者用表格列出trigger若干核心屬性。

在quartz源碼或注釋中,經(jīng)常使用fire(點(diǎn)火)這個(gè)動(dòng)詞來(lái)命名屬性名,表示觸發(fā)job。

屬性名 屬性類(lèi)型 說(shuō)明
name 所有trigger通用 trigger名稱(chēng)
group 所有trigger通用 trigger所屬的組名
description 所有trigger通用 trigger描述
calendarName 所有trigger通用 日歷名稱(chēng),指定使用哪個(gè)Calendar類(lèi),經(jīng)常用來(lái)從trigger的調(diào)度計(jì)劃中排除某些時(shí)間段
misfireInstruction 所有trigger通用 錯(cuò)過(guò)job(未在指定時(shí)間執(zhí)行的job)的處理策略,默認(rèn)為MISFIRE_INSTRUCTION_SMART_POLICY。詳見(jiàn)這篇blog^Quartz misfire
priority 所有trigger通用 優(yōu)先級(jí),默認(rèn)為5。當(dāng)多個(gè)trigger同時(shí)觸發(fā)job時(shí),線(xiàn)程池可能不夠用,此時(shí)根據(jù)優(yōu)先級(jí)來(lái)決定誰(shuí)先觸發(fā)
jobDataMap 所有trigger通用 同job的jobDataMap。假如job和trigger的jobDataMap有同名key,通過(guò)getMergedJobDataMap()獲取的jobDataMap,將以trigger的為準(zhǔn)
startTime 所有trigger通用 觸發(fā)開(kāi)始時(shí)間,默認(rèn)為當(dāng)前時(shí)間。決定什么時(shí)間開(kāi)始觸發(fā)job
endTime 所有trigger通用 觸發(fā)結(jié)束時(shí)間。決定什么時(shí)間停止觸發(fā)job
nextFireTime SimpleTrigger私有 下一次觸發(fā)job的時(shí)間
previousFireTime SimpleTrigger私有 上一次觸發(fā)job的時(shí)間
repeatCount SimpleTrigger私有 需觸發(fā)的總次數(shù)
timesTriggered SimpleTrigger私有 已經(jīng)觸發(fā)過(guò)的次數(shù)
repeatInterval SimpleTrigger私有 觸發(fā)間隔時(shí)間

TriggerBuilder類(lèi)

// 創(chuàng)建觸發(fā)器
// withIntervalInSeconds(2)表示每隔2s執(zhí)行任務(wù)
  Date triggerDate = new Date();
  SimpleScheduleBuilder schedBuilder = SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(2).repeatForever();
  TriggerBuilder<Trigger> triggerBuilder  = TriggerBuilder.newTrigger().withIdentity("myTrigger", "triggerGroup");
  Trigger trigger = triggerBuilder.startAt(triggerDate).withSchedule(schedBuilder).build();

上面代碼是demo一個(gè)片段,可以看出TriggerBuilder類(lèi)的作用:生成Trigger實(shí)例,默認(rèn)生成SimpleTriggerImpl實(shí)例。同JobBuilder一樣,這里也運(yùn)用了建造者模式。

5、scheduler(調(diào)度器)

scheduler主要由StdScheduler類(lèi)Scheduler接口StdSchedulerFactory類(lèi)SchedulerFactory接口QuartzScheduler類(lèi)實(shí)現(xiàn),它們的關(guān)系見(jiàn)下面UML圖。

scheduler UML圖
// 創(chuàng)建調(diào)度器
  SchedulerFactory schedulerFactory = new StdSchedulerFactory();
  Scheduler scheduler = schedulerFactory.getScheduler();
......
// 將任務(wù)及其觸發(fā)器放入調(diào)度器
  scheduler.scheduleJob(jobDetail, trigger);
// 調(diào)度器開(kāi)始調(diào)度任務(wù)
  scheduler.start();

上面代碼是demo一個(gè)片段,可以看出這里運(yùn)用了工廠(chǎng)模式,通過(guò)factory類(lèi)(StdSchedulerFactory)生產(chǎn)出scheduler實(shí)例(StdScheduler)。scheduler是整個(gè)quartz的關(guān)鍵,為此,筆者把demo中用到的scheduler接口的源碼加上中文注釋做個(gè)講解。

  • StdSchedulerFactory.getScheduler()源碼
public Scheduler getScheduler() throws SchedulerException {
        // 讀取quartz配置文件,未指定則順序遍歷各個(gè)path下的quartz.properties文件
        // 解析出quartz配置內(nèi)容和環(huán)境變量,存入PropertiesParser對(duì)象
        // PropertiesParser組合了Properties(繼承Hashtable),定義了一系列對(duì)Properties的操作方法,比如getPropertyGroup()批量獲取相同前綴的配置。配置內(nèi)容和環(huán)境變量存放在Properties成員變量中
        if (cfg == null) {
            initialize();
        }
        // 獲取調(diào)度器池,采用了單例模式
        // 其實(shí),調(diào)度器池的核心變量就是一個(gè)hashmap,每個(gè)元素key是scheduler名,value是scheduler實(shí)例
        // getInstance()用synchronized防止并發(fā)創(chuàng)建
        SchedulerRepository schedRep = SchedulerRepository.getInstance();

        // 從調(diào)度器池中取出當(dāng)前配置所用的調(diào)度器
        Scheduler sched = schedRep.lookup(getSchedulerName());
        
        ......

        // 如果調(diào)度器池中沒(méi)有當(dāng)前配置的調(diào)度器,則實(shí)例化一個(gè)調(diào)度器,主要?jiǎng)幼靼ǎ?        // 1)初始化threadPool(線(xiàn)程池):開(kāi)發(fā)者可以通過(guò)org.quartz.threadPool.class配置指定使用哪個(gè)線(xiàn)程池類(lèi),比如SimpleThreadPool。先class load線(xiàn)程池類(lèi),接著動(dòng)態(tài)生成線(xiàn)程池實(shí)例bean,然后通過(guò)反射,使用setXXX()方法將以org.quartz.threadPool開(kāi)頭的配置內(nèi)容賦值給bean成員變量;
        // 2)初始化jobStore(任務(wù)存儲(chǔ)方式):開(kāi)發(fā)者可以通過(guò)org.quartz.jobStore.class配置指定使用哪個(gè)任務(wù)存儲(chǔ)類(lèi),比如RAMJobStore。先class load任務(wù)存儲(chǔ)類(lèi),接著動(dòng)態(tài)生成實(shí)例bean,然后通過(guò)反射,使用setXXX()方法將以org.quartz.jobStore開(kāi)頭的配置內(nèi)容賦值給bean成員變量;
        // 3)初始化dataSource(數(shù)據(jù)源):開(kāi)發(fā)者可以通過(guò)org.quartz.dataSource配置指定數(shù)據(jù)源詳情,比如哪個(gè)數(shù)據(jù)庫(kù)、賬號(hào)、密碼等。jobStore要指定為JDBCJobStore,dataSource才會(huì)有效;
        // 4)初始化其他配置:包括SchedulerPlugins、JobListeners、TriggerListeners等;
        // 5)初始化threadExecutor(線(xiàn)程執(zhí)行器):默認(rèn)為DefaultThreadExecutor;
        // 6)創(chuàng)建工作線(xiàn)程:根據(jù)配置創(chuàng)建N個(gè)工作thread,執(zhí)行start()啟動(dòng)thread,并將N個(gè)thread順序add進(jìn)threadPool實(shí)例的空閑線(xiàn)程列表availWorkers中;
        // 7)創(chuàng)建調(diào)度器線(xiàn)程:創(chuàng)建QuartzSchedulerThread實(shí)例,并通過(guò)threadExecutor.execute(實(shí)例)啟動(dòng)調(diào)度器線(xiàn)程;
        // 8)創(chuàng)建調(diào)度器:創(chuàng)建StdScheduler實(shí)例,將上面所有配置和引用組合進(jìn)實(shí)例中,并將實(shí)例存入調(diào)度器池中
        sched = instantiate();

        return sched;
}

上面有個(gè)過(guò)程是初始化jobStore,表示使用哪種方式存儲(chǔ)scheduler相關(guān)數(shù)據(jù)。quartz有兩大jobStore:RAMJobStoreJDBCJobStoreRAMJobStore把數(shù)據(jù)存入內(nèi)存,性能最高,配置也簡(jiǎn)單,但缺點(diǎn)是系統(tǒng)掛了難以恢復(fù)數(shù)據(jù)。JDBCJobStore保存數(shù)據(jù)到數(shù)據(jù)庫(kù),保證數(shù)據(jù)的可恢復(fù)性,但性能較差且配置復(fù)雜。

兩種常見(jiàn)的任務(wù)存儲(chǔ)方式

  • QuartzScheduler.scheduleJob(JobDetail, Trigger)源碼
 public Date scheduleJob(JobDetail jobDetail,
            Trigger trigger) throws SchedulerException {
        // 檢查調(diào)度器是否開(kāi)啟,如果關(guān)閉則throw異常到上層
        validateState();
        ......
        // 獲取trigger首次觸發(fā)job的時(shí)間,以此時(shí)間為起點(diǎn),每隔一段指定的時(shí)間觸發(fā)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注冊(cè)進(jìn)調(diào)度器的jobStore
        resources.getJobStore().storeJobAndTrigger(jobDetail, trig);
        // 通知job監(jiān)聽(tīng)者
        notifySchedulerListenersJobAdded(jobDetail);                
        // 通知調(diào)度器線(xiàn)程
        notifySchedulerThread(trigger.getNextFireTime().getTime());
        // 通知trigger監(jiān)聽(tīng)者
        notifySchedulerListenersSchduled(trigger);

        return ft;
    }
  • QuartzScheduler.start()源碼
public void start() throws SchedulerException {
        ......
        // 這句最關(guān)鍵,作用是使調(diào)度器線(xiàn)程跳出一個(gè)無(wú)限循環(huán),開(kāi)始輪詢(xún)所有trigger觸發(fā)job
        // 原理詳見(jiàn)“如何采用多線(xiàn)程進(jìn)行任務(wù)調(diào)度”
        schedThread.togglePause(false);
        ......
    }

6、quartz線(xiàn)程模型

quartz運(yùn)行時(shí)線(xiàn)程圖

上圖是筆者在eclipse中調(diào)試demo時(shí)的線(xiàn)程圖,可以見(jiàn)到,除了第一條主線(xiàn)程外,還有10條工作線(xiàn)程,和1條調(diào)度器線(xiàn)程。

工作線(xiàn)程以{instanceName}_Worker-{[1-10]}命名。線(xiàn)程數(shù)目由quart.properties文件中的org.quartz.threadPool.threadCount配置項(xiàng)指定。所有工作線(xiàn)程都會(huì)放在線(xiàn)程池中,即所有工作線(xiàn)程都放在SimpleThreadPool實(shí)例的一個(gè)LinkedList<WorkerThread>成員變量中。WorkerThreadSimpleThreadPool的內(nèi)部類(lèi),這么設(shè)計(jì)可能是因?yàn)椴幌肜^承SimpleThreadPool但又想調(diào)用其protected方法,或者想隱藏WorkerThread。線(xiàn)程池還擁有兩個(gè)LinkedList<WorkerThread>:availWorkersbusyWorkers,分別存放空閑和正在執(zhí)行job的工作線(xiàn)程。

調(diào)度器線(xiàn)程以{instanceName}_QuartzSchedulerThread命名。該線(xiàn)程將根據(jù)trigger找出要待運(yùn)行job,然后從threadpool中拿出工作線(xiàn)程來(lái)執(zhí)行。調(diào)度器線(xiàn)程主體是QuartzSchedulerThread對(duì)象。

{instanceName}指的是quart.properties文件中的org.quartz.scheduler.instanceName配置值,這里是TestQuartzScheduler[1-10]表示從1到10的任意數(shù)字。

7、精彩源碼解讀

本節(jié)中,筆者從quartz源碼中挑選了兩段代碼,之所以選擇這兩段代碼,是因?yàn)樗鼈儗?shí)現(xiàn)了線(xiàn)程間通信、加鎖同步、避免GC等功能,對(duì)工程師們很有幫助。

如何采用多線(xiàn)程進(jìn)行任務(wù)調(diào)度

  • QuartzSchedulerThread.java
// 調(diào)度器線(xiàn)程一旦啟動(dòng),將一直運(yùn)行此方法
public void run() {
  ......
  // while()無(wú)限循環(huán),每次循環(huán)取出時(shí)間將到的trigger,觸發(fā)對(duì)應(yīng)的job,直到調(diào)度器線(xiàn)程被關(guān)閉
  // halted是一個(gè)AtomicBoolean類(lèi)變量,有個(gè)volatile int變量value,其get()方法僅僅簡(jiǎn)單的一句return value != 0,get()返回結(jié)果表示調(diào)度器線(xiàn)程是否開(kāi)關(guān)
  // volatile修飾的變量,存取必須走內(nèi)存,不能通過(guò)cpu緩存,這樣一來(lái)get總能獲得set的最新真實(shí)值,因此volatile變量適合用來(lái)存放簡(jiǎn)單的狀態(tài)信息
  // 顧名思義,AtomicBoolean要解決原子性問(wèn)題,但volatile并不能保證原子性,詳見(jiàn)http://blog.csdn.net/wxwzy738/article/details/43238089
  while (!halted.get()) {
     try {
        // check if we're supposed to pause...
        // sigLock是個(gè)Object對(duì)象,被用于加鎖同步
        // 需要用到wait(),必須加到synchronized塊內(nèi)
        synchronized (sigLock) {
            while (paused && !halted.get()) {
                try {
                    // wait until togglePause(false) is called...
                    // 這里會(huì)不斷循環(huán)等待,直到QuartzScheduler.start()調(diào)用了togglePause(false)
                    // 調(diào)用wait(),調(diào)度器線(xiàn)程進(jìn)入休眠狀態(tài),同時(shí)sigLock鎖被釋放
                    // togglePause(false)獲得sigLock鎖,將paused置為false,使調(diào)度器線(xiàn)程能夠退出此循環(huán),同時(shí)執(zhí)行sigLock.notifyAll()喚醒調(diào)度器線(xiàn)程
                    sigLock.wait(1000L);
                } catch (InterruptedException ignore) {}
            }
            ......
        }
        ......
        // 如果線(xiàn)程池中的工作線(xiàn)程個(gè)數(shù) > 0
        if(availThreadCount > 0) {
            ......
            // 獲取馬上到時(shí)間的trigger
            // 允許取出的trigger個(gè)數(shù)不能超過(guò)一個(gè)閥值,這個(gè)閥值是線(xiàn)程池個(gè)數(shù)與org.quartz.scheduler.batchTriggerAcquisitionMaxCount配置值間的最小者
            triggers = qsRsrcs.getJobStore().acquireNextTriggers(
                now + idleWaitTime, Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()), qsRsrcs.getBatchTimeWindow());
            ......
            // 執(zhí)行與trigger綁定的job
            // shell是JobRunShell對(duì)象,實(shí)現(xiàn)了Runnable接口
            // SimpleThreadPool.runInThread(Runnable)從線(xiàn)程池空閑列表中取出一個(gè)工作線(xiàn)程
            // 工作線(xiàn)程執(zhí)行WorkerThread.run(Runnable),詳見(jiàn)下方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();
        }
}

// 工作線(xiàn)程一旦啟動(dòng),將一直運(yùn)行此方法
@Override
public void run() {
        boolean ran = false;
        
        // 工作線(xiàn)程一直循環(huán)等待job,直到線(xiàn)程被關(guān)閉,原理同QuartzSchedulerThread.run()中的halted.get()
        while (run.get()) {
            try {
               // 原理同QuartzSchedulerThread.run()中的synchronized (sigLock)
               // 鎖住lock,不斷循環(huán)等待job,當(dāng)job要被執(zhí)行時(shí),WorkerThread.run(Runnable)被調(diào)用,job運(yùn)行環(huán)境被賦值給runnable
                synchronized(lock) {
                    while (runnable == null && run.get()) {
                        lock.wait(500);
                    }
                    // 開(kāi)始執(zhí)行job
                    if (runnable != null) {
                        ran = true;
                        // runnable.run()將觸發(fā)運(yùn)行job實(shí)現(xiàn)類(lèi)(比如JobImpl.execute())
                        runnable.run();
                    }
                }
            } catch (InterruptedException unblock) {
             ......
            }
        }
        ......
}

總的來(lái)說(shuō),核心代碼就是在while循環(huán)中調(diào)用Object.wait(),等待可以跳出while循環(huán)的條件成立,當(dāng)條件成立時(shí),立馬調(diào)度Object.notifyAll()使線(xiàn)程跳出while。通過(guò)這樣的代碼,可以實(shí)現(xiàn)調(diào)度器線(xiàn)程等待啟動(dòng)、工作線(xiàn)程等待job等功能。

如何避免GC

Quartz里提供了一種方案,用來(lái)避免某些對(duì)象被GC。方案其實(shí)簡(jiǎn)單而實(shí)用,就是QuartzScheduler類(lèi)創(chuàng)建了一個(gè)列表ArrayList<Object>(5) holdToPreventGC,如果某對(duì)象被add進(jìn)該列表,則意味著QuartzScheduler實(shí)例引用了此對(duì)象,那么此對(duì)象至少在QuartzScheduler實(shí)例存活時(shí)不會(huì)被GC。

哪些對(duì)象要避免GC?通過(guò)源碼可看到,調(diào)度器池和db管理器對(duì)象被放入了holdToPreventGC,但實(shí)際上兩種對(duì)象是static的,而static對(duì)象屬于GC root,應(yīng)該是不會(huì)被GC的,所以即使不放入holdToPreventGC,這兩種對(duì)象也不會(huì)被GC,除非被class unload或jvm生命結(jié)束。

static變量所指對(duì)象在heap中,如果變量不再指向該對(duì)象,比如賦值為null,對(duì)象會(huì)被GC


  1. https://github.com/quartz-scheduler/quartz ?

  2. http://www.quartz-scheduler.org ?

  3. https://github.com/star2478/java-quartz ?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,034評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 175,327評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀(guān)的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,554評(píng)論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,337評(píng)論 6 404
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 54,883評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評(píng)論 3 439
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,114評(píng)論 0 286
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,625評(píng)論 1 332
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,555評(píng)論 3 354
  • 茶點(diǎn)故事閱讀 42,737評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評(píng)論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,973評(píng)論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,362評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,615評(píng)論 1 280
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,343評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,699評(píng)論 2 370

推薦閱讀更多精彩內(nèi)容