深入 Java Timer 定时任务调度器实现原理

使用 Java 来调度定时任务时,我们经常会使用 Timer 类搞定。Timer 简单易用,其源码阅读起来也非常清晰,本节我们来仔细分析一下 Timer 类,来看看 JDK 源码的编写者是如何实现一个稳定可靠的简单调度器。

640?wx_fmt=gif

Timer 使用

Timer 调度任务有一次性调度和循环调度,循环调度有分为固定速率调度(fixRate)和固定时延调度(fixDelay)。固定速率就好比你今天加班到很晚,但是到了第二天还必须准点到公司上班,如果你一不小心加班到了第二天早上 9 点,你就连休息的时间都没有了。而固定时延的意思是你必须睡够 8 个小时再过来上班,如果你加班到凌晨 6 点,那就可以下午过来上班了。固定速率强调准点,固定时延强调间隔。

Timer timer = new Timer();

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

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

内部结构

Timer 类里包含一个任务队列和一个异步轮训线程。任务队列里容纳了所有待执行的任务,所有的任务将会在这一个异步线程里执行,切记任务的执行代码不可以抛出异常,否则会导致 Timer 线程挂掉,所有的任务都没得执行了。单个任务也不易执行时间太长,否则会影响任务调度在时间上的精准性。比如你一个任务跑了太久,其它等着调度的任务就一直处于饥饿状态得不到调度。所有任务的执行都是这单一的 TimerThread 线程。

class Timer {
  TaskQueue queue = new TaskQueue();
  TimerThread thread = new TimerThread(queue);
}
640?wx_fmt=gif
堆排序

Timer 的任务队列 TaskQueue 是一个特殊的队列,它内部是一个数组。这个数组会按照待执行时间进行堆排序,堆顶元素总是待执行时间最小的任务。轮训线程会每次轮训出时间点最近的并且到点的任务来执行。数组会自动扩容,如果任务非常多。

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

synchronized(queue) {
  ...
}

任务状态

TimerTask 有 4 个状态,VIRGIN 是默认状态,刚刚实例化还没有被调度。SCHEDULED 表示已经将任务塞进 TaskQueue 等待被执行。EXECUTED 表示任务已经执行完成。CANCELLED 表示任务被取消了,还没来得及执行就被人为取消了。

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; // 下次执行时间
  long period = 0; // 间隔
}

对于一个循环任务来说,它不存在 EXECUTED 状态,因为它每次刚刚执行完成,就被重新调度了。EXECUTED 状态仅仅存在于一次性任务,而且这个状态其实并不是表示任务已经执行完成,它是指已经从任务队列里摘出来了,马上就要执行。

任务间隔字段 period 比较特殊,当使用固定速率时,period 为正值,当使用固定间隔时,period 为负值,当任务是一次性时,period 为零。下面是循环任务的下次调度时间设定

currentTime = System.currentTimeMillis();
executionTime = task.nextExecutionTime;
// 固定时延基于 currentTime 顺延
// 固定速率基于 executionTime(设定时间) 顺延
// next_exec_time = exec_time + period = first_delay + n * period
queue.rescheduleMin(
      task.period<0 ? currentTime   - task.period  
                    : executionTime + task.period);

任务锁

Timer 的任务支持取消操作,取消任务的线程和执行任务的线程极有可能不是一个线程。有可能任务正在执行中,结果另一个线程表示要取消任务。这时候 Timer 是如何处理的呢?在 TimerTask 类里看到了一把锁。当任务属性需要修改的时候,都会加锁。

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

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

// 调度任务
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;
 }
}

// 运行任务
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();
  }
}

public int purge() {
    int result = 0;
    // 灭掉 CANCELLED 状态的任务
    synchronized(queue) {
        for (int i = queue.size(); i > 0; i--) {
             if (queue.get(i).state == TimerTask.CANCELLED) {
                queue.quickRemove(i);
                result++;
             }
         }
    }
    // 堆调整
    if (result != 0)
         queue.heapify();
    }
    return result;
}

任务队列空了

任务队列里没有任务了,调度线程必须按一定的策略进行睡眠。它需要睡眠一直到最先执行的任务到点时立即醒来,所以睡眠截止时间就是第一个任务将要执行的时间。同时在睡觉的时候,有可能会有新的任务被添加进来,它的调度时间可能会更加提前,所以当有新的任务到来时需要可以唤醒正在睡眠的线程。

private void mainLoop() {
  while(true) {
    ...
    task = queue.getMin();
    currentTime = System.currentTimeMillis();
    executionTime = task.nextExecutionTime;
    if(executionTime > currentTime) {
      // 开始睡大觉
      queue.wait(executionTime - currentTime);
    }
    ...
  }
}

// 新任务进来了
private void sched(TimerTask task, long time, long period) {
   ...
   queue.add(task);
   if (queue.getMin() == task)
        queue.notify();  // 唤醒轮训线程
}

Timer 终止

Timer 提供了 cancel() 方法清空队列,停止调度器,不允许有任何新任务进来。它会将 newTasksMayBeScheduled 字段设置为 false 表示 Timer 即将终止。

class TimerThread {
  ...
  boolean newTasksMayBeScheduled;  // 终止的标志
  ...
}

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

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

private void mainLoop() {
   while(true) {
      // 正常清空下,队列空了,轮训线程会休眠
      // 但是如果 newTasksMayBeScheduled 为 false
      // 那么循环会退出,轮训线程会终止
      while (queue.isEmpty() && newTasksMayBeScheduled)
          queue.wait();
      if (queue.isEmpty())
          break;
      ...
   }
}

垃圾回收

还有一个特殊的场景需要特别注意,那就是当轮训线程因为队列里没有任务而睡眠的时候,Timer 对象因为不再被引用而被垃圾回收了。这时候需要主动唤醒轮训线程,让它退出。

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

640?wx_fmt=png

阅读更多精品文章,长按二维码识别关注公众号「码洞」

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值