溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

怎么深入Java Timer 定時(shí)任務(wù)調(diào)度器實(shí)現(xiàn)原理

發(fā)布時(shí)間:2021-12-18 17:27:19 來(lái)源:億速云 閱讀:167 作者:柒染 欄目:編程語(yǔ)言

這篇文章將為大家詳細(xì)講解有關(guān)怎么深入Java Timer 定時(shí)任務(wù)調(diào)度器實(shí)現(xiàn)原理,文章內(nèi)容質(zhì)量較高,因此小編分享給大家做個(gè)參考,希望大家閱讀完這篇文章后對(duì)相關(guān)知識(shí)有一定的了解。

使用 Java 來(lái)調(diào)度定時(shí)任務(wù)時(shí),我們經(jīng)常會(huì)使用 Timer 類(lèi)搞定。Timer 簡(jiǎn)單易用,其源碼閱讀起來(lái)也非常清晰,本節(jié)我們來(lái)仔細(xì)分析一下 Timer 類(lèi),來(lái)看看 JDK 源碼的編寫(xiě)者是如何實(shí)現(xiàn)一個(gè)穩(wěn)定可靠的簡(jiǎn)單調(diào)度器。

Timer 使用

Timer 調(diào)度任務(wù)有一次性調(diào)度和循環(huán)調(diào)度,循環(huán)調(diào)度有分為固定速率調(diào)度(fixRate)和固定時(shí)延調(diào)度(fixDelay)。固定速率就好比你今天加班到很晚,但是到了第二天還必須準(zhǔn)點(diǎn)到公司上班,如果你一不小心加班到了第二天早上 9 點(diǎn),你就連休息的時(shí)間都沒(méi)有了。而固定時(shí)延的意思是你必須睡夠 8 個(gè)小時(shí)再過(guò)來(lái)上班,如果你加班到凌晨 6 點(diǎn),那就可以下午過(guò)來(lái)上班了。固定速率強(qiáng)調(diào)準(zhǔn)點(diǎn),固定時(shí)延強(qiáng)調(diào)間隔。

Timer timer = new Timer();

TimerTask task = new TimerTask() {
  public void run() {
    System.out.println("wtf");
  }
};

// 延遲 1s 打印 wtf 一次
timer.schedule(task, 1000)
// 延遲 1s 固定時(shí)延每隔 1s 周期打印一次 wtf
timer.schedule(task, 1000, 1000);
// 延遲 1s 固定速率每隔 1s 周期打印一次 wtf
timer.scheduleAtFixRate(task, 1000, 1000)


如果你有一個(gè)任務(wù)必須每天準(zhǔn)點(diǎn)調(diào)度,那就應(yīng)該使用固定速率調(diào)度,并且要確保每個(gè)任務(wù)執(zhí)行時(shí)間不要太長(zhǎng),千萬(wàn)別超過(guò)了第二天這個(gè)點(diǎn)。如果你有一個(gè)任務(wù)需要每隔幾分鐘跑一次,那就使用固定時(shí)延調(diào)度,它不是很在乎你的單個(gè)任務(wù)要跑多長(zhǎng)時(shí)間。

內(nèi)部結(jié)構(gòu)

Timer 類(lèi)里包含一個(gè)任務(wù)隊(duì)列和一個(gè)異步輪訓(xùn)線程。任務(wù)隊(duì)列里容納了所有待執(zhí)行的任務(wù),所有的任務(wù)將會(huì)在這一個(gè)異步線程里執(zhí)行,切記任務(wù)的執(zhí)行代碼不可以拋出異常,否則會(huì)導(dǎo)致 Timer 線程掛掉,所有的任務(wù)都沒(méi)得執(zhí)行了。單個(gè)任務(wù)也不易執(zhí)行時(shí)間太長(zhǎng),否則會(huì)影響任務(wù)調(diào)度在時(shí)間上的精準(zhǔn)性。比如你一個(gè)任務(wù)跑了太久,其它等著調(diào)度的任務(wù)就一直處于饑餓狀態(tài)得不到調(diào)度。所有任務(wù)的執(zhí)行都是這單一的 TimerThread 線程。

class Timer {
  TaskQueue queue = new TaskQueue();
  TimerThread thread = new TimerThread(queue);
}
怎么深入Java Timer 定時(shí)任務(wù)調(diào)度器實(shí)現(xiàn)原理


Timer 的任務(wù)隊(duì)列 TaskQueue 是一個(gè)特殊的隊(duì)列,它內(nèi)部是一個(gè)數(shù)組。這個(gè)數(shù)組會(huì)按照待執(zhí)行時(shí)間進(jìn)行堆排序,堆頂元素總是待執(zhí)行時(shí)間最小的任務(wù)。輪訓(xùn)線程會(huì)每次輪訓(xùn)出時(shí)間點(diǎn)最近的并且到點(diǎn)的任務(wù)來(lái)執(zhí)行。數(shù)組會(huì)自動(dòng)擴(kuò)容,如果任務(wù)非常多。

class TaskQueue {
  TimerTask[] queue = new TimerTask[128];
  int size;
}


任意線程都可以通過(guò) Timer.schedule 方法將任務(wù)加入 TaskQueue,但是 TaskQueue 又并不是線程安全的數(shù)據(jù)結(jié)構(gòu)。所在每次修改 TaskQueue 時(shí)都需要加鎖。

synchronized(queue) {
  ...
}

任務(wù)狀態(tài)

TimerTask 有 4 個(gè)狀態(tài),VIRGIN 是默認(rèn)狀態(tài),剛剛實(shí)例化還沒(méi)有被調(diào)度。SCHEDULED 表示已經(jīng)將任務(wù)塞進(jìn) TaskQueue 等待被執(zhí)行。EXECUTED 表示任務(wù)已經(jīng)執(zhí)行完成。CANCELLED 表示任務(wù)被取消了,還沒(méi)來(lái)得及執(zhí)行就被人為取消了。

abstract class TimerTask {
  int state = VIRGIN;
  static final int VIRGIN = 0;
  static final int SCHEDULED = 1;
  static final int EXECUTED = 2;
  static final int CANCELLED = 3;

  long nextExecutionTime; // 下次執(zhí)行時(shí)間
  long period = 0; // 間隔
}

對(duì)于一個(gè)循環(huán)任務(wù)來(lái)說(shuō),它不存在 EXECUTED 狀態(tài),因?yàn)樗看蝿倓倛?zhí)行完成,就被重新調(diào)度了。EXECUTED 狀態(tài)僅僅存在于一次性任務(wù),而且這個(gè)狀態(tài)其實(shí)并不是表示任務(wù)已經(jīng)執(zhí)行完成,它是指已經(jīng)從任務(wù)隊(duì)列里摘出來(lái)了,馬上就要執(zhí)行。

任務(wù)間隔字段 period 比較特殊,當(dāng)使用固定速率時(shí),period 為正值,當(dāng)使用固定間隔時(shí),period 為負(fù)值,當(dāng)任務(wù)是一次性時(shí),period 為零。下面是循環(huán)任務(wù)的下次調(diào)度時(shí)間設(shè)定

currentTime = System.currentTimeMillis();
executionTime = task.nextExecutionTime;
// 固定時(shí)延基于 currentTime 順延
// 固定速率基于 executionTime(設(shè)定時(shí)間) 順延
// next_exec_time = exec_time + period = first_delay + n * period
queue.rescheduleMin(
      task.period<0 ? currentTime   - task.period  
                    : executionTime + task.period);


對(duì)于固定速率來(lái)說(shuō),如果任務(wù)執(zhí)行時(shí)間太長(zhǎng)超出了間隔,那么它可能會(huì)持續(xù)霸占任務(wù)隊(duì)列,因?yàn)樗恼{(diào)度時(shí)間將總是低于 currentTime,排在堆頂,每次輪訓(xùn)取出來(lái)的都是它。運(yùn)行完畢后,重新調(diào)度這個(gè)任務(wù),它的時(shí)間依舊趕不上。持續(xù)下去你會(huì)看到這個(gè)任務(wù)的調(diào)度時(shí)間遠(yuǎn)遠(yuǎn)落后于當(dāng)前時(shí)間,而其它任務(wù)可能會(huì)徹底餓死。這就是為什么一定要特別注意固定速率的循環(huán)任務(wù)運(yùn)行時(shí)間不宜過(guò)長(zhǎng)。

任務(wù)鎖

Timer 的任務(wù)支持取消操作,取消任務(wù)的線程和執(zhí)行任務(wù)的線程極有可能不是一個(gè)線程。有可能任務(wù)正在執(zhí)行中,結(jié)果另一個(gè)線程表示要取消任務(wù)。這時(shí)候 Timer 是如何處理的呢?在 TimerTask 類(lèi)里看到了一把鎖。當(dāng)任務(wù)屬性需要修改的時(shí)候,都會(huì)加鎖。

abstract class TimerTask {
  final Object lock = new Object();
}

// 取消任務(wù)
public boolean cancel() {
    synchronized(lock) {
        boolean result = (state == SCHEDULED);
        state = CANCELLED;
        return result;
    }
}

// 調(diào)度任務(wù)
private void sched(TimerTask task, long time, long period) {
  synchronized(task.lock) {
    if (task.state != TimerTask.VIRGIN)
        throw new IllegalStateException(
              "Task already scheduled or cancelled");
    task.nextExecutionTime = time;
    task.period = period;
    task.state = TimerTask.SCHEDULED;
 }
}

// 運(yùn)行任務(wù)
private void mainLoop() {
  while(true) {
    synchronized(task.lock) {
        if (task.state == TimerTask.CANCELLED) {
            queue.removeMin();
            continue;
        }
        ...
        if(task.period == 0) {
          task.state = TimerTask.EXECUTED;
        } 
        ...
    }
    task.run();
  }
}


在任務(wù)運(yùn)行之前會(huì)檢查任務(wù)是不是已經(jīng)被取消了,如果取消了,就從隊(duì)列中移除。一旦任務(wù)開(kāi)始運(yùn)行 run(),對(duì)于單次任務(wù)來(lái)說(shuō)它就無(wú)法被取消了,而循環(huán)任務(wù)將不會(huì)繼續(xù)下次調(diào)度。如果任務(wù)沒(méi)有機(jī)會(huì)得到執(zhí)行(時(shí)間設(shè)置的太長(zhǎng)),那么即使這個(gè)任務(wù)被取消了,它也會(huì)一直持續(xù)躺在任務(wù)隊(duì)列中。設(shè)想如果你調(diào)度了一系列久遠(yuǎn)的任務(wù),然后都取消了,這可能會(huì)成為一個(gè)內(nèi)存泄露點(diǎn)。所以 Timer 還單獨(dú)提供了一個(gè) purge() 方法可以一次性清空所有的已取消的任務(wù)。

public int purge() {
    int result = 0;
    // 滅掉 CANCELLED 狀態(tài)的任務(wù)
    synchronized(queue) {
        for (int i = queue.size(); i > 0; i--) {
             if (queue.get(i).state == TimerTask.CANCELLED) {
                queue.quickRemove(i);
                result++;
             }
         }
    }
    // 堆調(diào)整
    if (result != 0)
         queue.heapify();
    }
    return result;
}

任務(wù)隊(duì)列空了

任務(wù)隊(duì)列里沒(méi)有任務(wù)了,調(diào)度線程必須按一定的策略進(jìn)行睡眠。它需要睡眠一直到最先執(zhí)行的任務(wù)到點(diǎn)時(shí)立即醒來(lái),所以睡眠截止時(shí)間就是第一個(gè)任務(wù)將要執(zhí)行的時(shí)間。同時(shí)在睡覺(jué)的時(shí)候,有可能會(huì)有新的任務(wù)被添加進(jìn)來(lái),它的調(diào)度時(shí)間可能會(huì)更加提前,所以當(dāng)有新的任務(wù)到來(lái)時(shí)需要可以喚醒正在睡眠的線程。

private void mainLoop() {
  while(true) {
    ...
    task = queue.getMin();
    currentTime = System.currentTimeMillis();
    executionTime = task.nextExecutionTime;
    if(executionTime > currentTime) {
      // 開(kāi)始睡大覺(jué)
      queue.wait(executionTime - currentTime);
    }
    ...
  }
}

// 新任務(wù)進(jìn)來(lái)了
private void sched(TimerTask task, long time, long period) {
   ...
   queue.add(task);
   if (queue.getMin() == task)
        queue.notify();  // 喚醒輪訓(xùn)線程
}


代碼中的 wait() 方法就是調(diào)用了 Object.wait() 來(lái)進(jìn)行睡眠。當(dāng)有新任務(wù)進(jìn)來(lái)了,發(fā)現(xiàn)這個(gè)新任務(wù)的運(yùn)行時(shí)間是最早的,那就調(diào)用 notify() 方法喚醒輪訓(xùn)線程。

Timer 終止

Timer 提供了 cancel() 方法清空隊(duì)列,停止調(diào)度器,不允許有任何新任務(wù)進(jìn)來(lái)。它會(huì)將 newTasksMayBeScheduled 字段設(shè)置為 false 表示 Timer 即將終止。

class TimerThread {
  ...
  boolean newTasksMayBeScheduled;  // 終止的標(biāo)志
  ...
}

public void cancel() {
    synchronized(queue) {
        thread.newTasksMayBeScheduled = false;
        queue.clear();
        queue.notify();
    }
}


如果 Timer 終止了,還有新任務(wù)進(jìn)來(lái)就會(huì)拋出異常。

private void sched(TimerTask task, long time, long period) {
  synchronized(queue) {
    if (!thread.newTasksMayBeScheduled)
       throw new IllegalStateException("Timer already cancelled.");
    ...
  }
}


我們還注意到 Timer.cancel() 方法會(huì)喚醒輪訓(xùn)線程,為的是可以立即停止輪訓(xùn)。不過(guò)如果任務(wù)正在執(zhí)行中,這之后 cancel() 就必須等到任務(wù)執(zhí)行完畢才可以停止。

private void mainLoop() {
   while(true) {
      // 正常清空下,隊(duì)列空了,輪訓(xùn)線程會(huì)休眠
      // 但是如果 newTasksMayBeScheduled 為 false
      // 那么循環(huán)會(huì)退出,輪訓(xùn)線程會(huì)終止
      while (queue.isEmpty() && newTasksMayBeScheduled)
          queue.wait();
      if (queue.isEmpty())
          break;
      ...
   }
}

垃圾回收

還有一個(gè)特殊的場(chǎng)景需要特別注意,那就是當(dāng)輪訓(xùn)線程因?yàn)殛?duì)列里沒(méi)有任務(wù)而睡眠的時(shí)候,Timer 對(duì)象因?yàn)椴辉俦灰枚焕厥樟恕_@時(shí)候需要主動(dòng)喚醒輪訓(xùn)線程,讓它退出。

class Timer {
  ...
  private final Object threadReaper = new Object() {
        @SuppressWarnings("deprecation")
        protected void finalize() throws Throwable {
            synchronized(queue) {
                thread.newTasksMayBeScheduled = false;
                queue.notify();
            }
        }
  };
  ...


當(dāng) Timer 被回收時(shí),內(nèi)部字段 threadPeaper 指向的對(duì)象也會(huì)被回收。所以 finalize 方法將會(huì)被調(diào)用,喚醒并終止 Timer 輪訓(xùn)線程。如果沒(méi)有這個(gè) threadPeaper 對(duì)象就可能會(huì)導(dǎo)致 JVM 里留下僵尸線程。

關(guān)于怎么深入Java Timer 定時(shí)任務(wù)調(diào)度器實(shí)現(xiàn)原理就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺(jué)得文章不錯(cuò),可以把它分享出去讓更多的人看到。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI