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

转自----------------------- Page 1-----------------------

2018­12­19                               

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

  原创:老钱 码洞 前天 

  使用 Java 来调度定时任务时,我们经常会使用 Timer 类搞定。Timer 简单易用,其源 

  码阅读起来也非常清晰,本节我们来仔细分析一下 Timer 类,来看看 JDK 源码的编写者 

  是如何实现一个稳定可靠的简单调度器。 

  Timer 使用 


----------------------- Page 2-----------------------

2018­12­19                                   

  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);  
    }  


----------------------- Page 3-----------------------

2018­12­19                                          

                                             堆排序 

   Timer 的任务队列 TaskQueue 是一个特殊的队列,它内部是一个数组。这个数组会按照 

   待执行时间进行堆排序,堆顶元素总是待执行时间最小的任务。轮训线程会每次轮训出时间 

   点最近的并且到点的任务来执行。数组会自动扩容,如果任务非常多。 

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

   任 意 线 程 都 可 以 通 过   Timer.schedule  方 法 将 任 务 加 入   TaskQueue ,但 是 

   TaskQueue 又并不是线程安全的数据结构。所在每次修改 TaskQueue 时都需要加锁。 

    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; // 下次执行时间  


----------------------- Page 4-----------------------

2018­12­19                                    

      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);  

  对于固定速率来说,如果任务执行时间太长超出了间隔,那么它可能会持续霸占任务队列, 

   因为它的调度时间将总是低于 currentTime,排在堆顶,每次轮训取出来的都是它。运行 

  完毕后,重新调度这个任务,它的时间依旧赶不上。持续下去你会看到这个任务的调度时间 

  远远落后于当前时间,而其它任务可能会彻底饿死。这就是为什么一定要特别注意固定速率 

   的循环任务运行时间不宜过长。 

  任务锁 

  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) {  


----------------------- Page 5-----------------------

2018­12­19                                             

      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();  
      }  
    }  

   在任务运行之前会检查任务是不是已经被取消了,如果取消了,就从队列中移除。一旦任务 

   开始运行  run(),对于单次任务来说它就无法被取消了,而循环任务将不会继续下次调度。 

   如果任务没有机会得到执行 (时间设置的太长),那么即使这个任务被取消了,它也会一直 

   持续躺在任务队列中。设想如果你调度了一系列久远的任务,然后都取消了,这可能会成为 

   一个内存泄露点。所以 Timer 还单独提供了一个  purge() 方法可以一次性清空所有的已 

   取消的任务。 

    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;  
    }  


----------------------- Page 6-----------------------

2018­12­19                                          

   任务队列空了 

   任务队列里没有任务了,调度线程必须按一定的策略进行睡眠。它需要睡眠一直到最先执行 

   的任务到点时立即醒来,所以睡眠截止时间就是第一个任务将要执行的时间。同时在睡觉的 

   时候,有可能会有新的任务被添加进来,它的调度时间可能会更加提前,所以当有新的任务 

   到来时需要可以唤醒正在睡眠的线程。 

    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();  // 唤醒轮训线程  
    }  

   代码中的 wait() 方法就是调用了 Object.wait() 来进行睡眠。当有新任务进来了,发现 

   这个新任务的运行时间是最早的,那就调用  notify() 方法唤醒轮训线程。 

   Timer 终止 

   Timer 提供了 cancel() 方法清空队列,停止调度器,不允许有任何新任务进来。它会将 

   newTasksMayBeScheduled 字段设置为 false 表示 Timer 即将终止。 

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

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


----------------------- Page 7-----------------------

2018­12­19                                             

            queue.notify();  
        }  
    }  

   如果 Timer 终止了,还有新任务进来就会抛出异常。 

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

   我们还注意到 Timer.cancel() 方法会唤醒轮训线程,为的是可以立即停止轮训。不过如 

   果任务正在执行中,这之后 cancel() 就必须等到任务执行完毕才可以停止。 

    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();  
                }  
            }  
      };  
      ...  
    }   


----------------------- Page 8-----------------------

2018­12­19                                     

   当 Timer 被回收时,内部字段 threadPeaper 指向的对象也会被回收。所以 finalize 

   方法将会被调用,唤醒并终止 Timer 轮训线程。如果没有这个 threadPeaper 对象就可 

   能会导致 JVM 里留下僵尸线程。 

 


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值