細數那些使用過的定時器

前言

何爲定時器,說白了就是指定一個延遲時間,到期執行,就像咱們早上定的鬧鈴同樣,天天定點提醒咱們起牀;固然在咱們各個系統中也是無處不在,好比定時備份數據,定時拉取文件,定時刷新數據等等;定時器工具也是層出不窮好比Timer,ScheduledExecutorService,Spring Scheduler,HashedWheelTimer(時間輪),Quartz,Xxl-job/Elastic-job等;本文將對這些定時器工具作個簡單介紹和對比,都在哪些場景下使用。mysql

Timer

Timer能夠說是JDK提供最先的一個定時器了,使用簡單,功能也相對來講比較簡單;能夠指定固定時間後觸發,固定時間點觸發,固定頻率觸發;算法

Timer timer = new Timer();
timer.schedule(new TimerTask() {
    @Override
    public void run() {
        System.out.println(System.currentTimeMillis() + "  === task1");
    }
}, 1000, 1000);

時間默認爲毫秒,表示延遲一秒後執行任務,而且頻率爲1秒執行任務;Timer內部使用TaskQueue存聽任務,使用TimerThread單線程用來執行任務:spring

private final TaskQueue queue = new TaskQueue();
private final TimerThread thread = new TimerThread(queue);

TimerThread內部是一個while(true)循環,不停的從TaskQueue中獲取任務執行;固然每次添加到TaskQueue中的任務會進行排序,經過nextExecutionTime來進行排序,這樣TimerThread每次均可以獲取到最近執行的任務;
Timer有兩大缺點:sql

  • TimerTask中出現未捕獲的異常,影響Timer;
  • 由於是單線程執行某個任務執行時間過長會影響其餘認爲的精確度;

正由於Timer存在的一些缺點,JDK1.5出現了新的定時器ScheduledExecutorService;數據庫

ScheduledExecutorService

JDK1.5提供了線程池的功能,ScheduledExecutorService是一個接口類,具體實現類是ScheduledThreadPoolExecutor繼承於ThreadPoolExecutor;segmentfault

ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
    @Override
    public void run() {
        System.out.println(Thread.currentThread() + " === " + System.currentTimeMillis() + " === task1");
    }
}, 1000, 1000, TimeUnit.MILLISECONDS);

比起Timer能夠配置多個線程去執行定時任務,同時異常任務並不會中斷ScheduledExecutorService,線程池的幾個核心配置:多線程

  • corePoolSize:核心線程數量,若是線程池中的線程少於此數目,則在執行任務時建立,核心線程不會被回收;
  • maximumPoolSize:容許的最大線程數,當線程數量達到corePoolSize且workQueue隊列滿了,會建立線程;
  • keepAliveTime:超過corePoolSize空閒時間;
  • unit:keepAliveTime的單位;
  • workQueue:當線程超過corePoolSize,新的任務會被加入到隊列中等待;
  • threadFactory:建立線程的工廠類;
  • handler:線程池拒絕策略,包括:AbortPolicy,DiscardPolicy,DiscardOldestPolicy,CallerRunsPolicy策略,固然也能夠本身擴展;

ScheduledExecutorService中添加的任務會被包裝成一個ScheduledFutureTask類,同時將任務放入DelayedWorkQueue隊列中是一個BlockingQueue;相似Timer也會根據加入任務觸發時間的前後進行排序,而後線程池中的Worker會到Queue中獲取任務執行;運維

Spring Scheduler

Spring提供了xml和註解方式來配置調度任務,以下面xml配置:分佈式

<!-- 建立一個調度器 -->
<task:scheduler id="scheduler" />
<!-- 配置任務類的bean -->
<bean id="helloTask" class="com.spring.task.HelloTask"></bean>
<task:scheduled-tasks scheduler="scheduler">
    <!-- 每2秒執行一次 -->
    <task:scheduled ref="helloTask" method="say" cron="0/2 * * * * ?" />
</task:scheduled-tasks>

Spring提供了cron表達式的支持,而且能夠直接配置執行指定類中的指定方法,對使用者來講更加方便和簡單;可是其內部仍是使用的ScheduledThreadPoolExecutor線程池;ide

HashedWheelTimer

Netty提供的一個定時器,用於定時發送心跳,使用的是時間輪算法;HashedWheelTimer是一個環形結構,能夠類比成一個時鐘,整個環形結構由一個個小格組成,每一個小格能夠存放不少任務,隨着時間的流逝,指針轉動,而後執行當前指定格子中的任務;任務經過取模的方式決定其應該放在哪一個格子,有點相似hashmap;

HashedWheelTimer hashedWheelTimer = new HashedWheelTimer(1000, TimeUnit.MILLISECONDS, 16);
hashedWheelTimer.newTimeout(new TimerTask() {
    @Override
    public void run(Timeout timeout) throws Exception {
        System.out.println(System.currentTimeMillis() + "  === executed");
    }
}, 1, TimeUnit.SECONDS);

其中初始化的三個參數分別是:

  • tickDuration:每一格的時長;
  • unit:tickDuration的單位;
  • ticksPerWheel:時間輪總共有多少格;

如上面實例配置的參數,每一格時長1秒,時間輪總共16格,若是延遲1秒執行,那就放到編號1的格子中,從0開始;若是延遲18秒,那麼會放到編號爲2的格子中,同時指定remainingRounds=1,表示第幾輪被調用,每轉一輪remainingRounds-1,知道remainingRounds=0纔會被執行;

Quartz

以上介紹的幾種定時器都是進程內的調度,而Quartz提供了分佈式調度,全部被調度的任務均可以存放到數據庫中,每一個業務節點經過搶佔式的方式去獲取須要執行的任務,其中一個節點出現問題並不影響任務的調度;

<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
    destroy-method="close">
    <property name="driverClass" value="com.mysql.jdbc.Driver" />
    <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/quartz" />
    <property name="user" value="root" />
    <property name="password" value="root" />
</bean>
<bean id="scheduler"
    class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
    <property name="schedulerName" value="myScheduler"></property>
    <property name="dataSource" ref="dataSource" />
    <property name="configLocation" value="classpath:quartz.properties" />
    <property name="triggers">
        <list>
            <ref bean="firstCronTrigger" />
        </list>
    </property>
</bean>

更多關於Quartz的介紹能夠參考本人以前的文章:

固然Quartz自己也有不足的地方:底層調度依賴數據庫的悲觀鎖,誰先搶到誰調度,這樣會致使節點負載不均衡;還有調度和執行耦合在一塊兒,致使調度器會受到業務的影響;

Xxl-job/Elastic-job

正由於Quartz存在着不少不足的地方,基於Quartz實現的分佈式調度解決方案出現了包括Xxl-job/Elastic-job等;
總體思路:調度器和執行器拆成不一樣的進程,調度器仍是依賴Quartz自己的調度方式,可是調度的並非具體業務的QuartzJobBean,而是統一的一個RemoteQuartzJobBean,在此Bean中經過遠程調用執行器去執行具體業務Bean;具體的執行器在啓動時註冊到註冊中心(如Zookeeper)中,調度器能夠在註冊中心(如Zookeeper)獲取執行器信息,並經過相關的負載算法指定具體的執行器去執行;
還提供了運維管理界面,能夠管理任務,好比像xxl-job:
輸入圖片說明

固然還有更多其餘的功能,此處就不在介紹了,能夠直接去查看官網;

選擇合適的定時器

其實總體能夠分爲兩大類:進程內定時器包括和分佈式調度器;
進程內定時器:Timer,ScheduledExecutorService,Spring Scheduler,HashedWheelTimer(時間輪);
分佈式調度器:Quartz,Xxl-job/Elastic-job;因此首先根據須要僅僅只須要進程內的定時器,仍是須要分佈式調度;其次在進程內Timer基本能夠被淘汰了,徹底可使用ScheduledExecutorService來代替,若是系統使用了Spring那固然應該使用Spring Scheduler;下面重點看看ScheduledExecutorService和HashedWheelTimer,ScheduledExecutorService內部使用的是DelayedWorkQueue,任務的新增、刪除會致使性能降低;而HashedWheelTimer並不受任務數量限制,因此若是任務不少而且任務執行時間很短好比心跳,那麼HashedWheelTimer是最好的選擇;HashedWheelTimer是單線程的,若是任務很少而且執行時間過長,影響精確度,而ScheduledExecutorService可使用多線程這時候選擇ScheduledExecutorService更好;最後分佈式調度器裏面Quartz和Xxl-job/Elastic-job,對分佈式調度要求不高的狀況下才會選擇Quartz,否則都應該選擇Xxl-job/Elastic-job。

相關文章
相關標籤/搜索