从源码角度学习Timer定时器

Timer定时器

通过一个后台线程(成员变量thread)来执行任务调度,每个Timer对象对应的是一个单一的后台线程,用于执行所有的定时器的任务,所以,定时器的任务应该很快完成,如果某个任务执行花费过多时间,它会长时间占用该线程,导致其他任务延迟执行以及扎堆执行。

这个类是线程安全的,多个线程可以共享给一个Timer对象,而不需要外部进行同步。


Timer

1. 成员变量

任务队列,参考TaskQueue

 private final TaskQueue queue = new TaskQueue();

执行任务调度的线程,参考TimerThread

private final TimerThread thread = new TimerThread(queue);

这个对象可以理解为线程终结者。这个引用只是纯定义在Timer中,没有再次使用,目的就是在Timer回收之前,优先执行这个引用复写的finalize方法。方法的内容是将变量 newTasksMayBeScheduled (参考TimerThread)设定为false,同时唤醒timerthread线程。

    private final Object threadReaper = new Object() {
        protected void finalize() throws Throwable {
            synchronized(queue) {
                thread.newTasksMayBeScheduled = false;
                queue.notify(); // In case queue is empty.
            }
        }
    };

2. 构造方法

所有构造函数都会启动计时器内部线程thread,有4个构造方法,比较简单没什么说的

  public Timer(String name) {
        thread.setName(name);
        thread.start();
    }

3. 核心方法

3.1 任务调度

6个schedule方法其实也没什么好说的,重点在于内部私有方法sched

 1.  // 延迟 delay 毫秒后执行 task 任务
   public void schedule(TimerTask task, long delay) {
        if (delay < 0)
            throw new IllegalArgumentException("Negative delay.");
        sched(task, System.currentTimeMillis()+delay, 0);
    }

 2.   // 指定时间点 time 执行 task 任务
    public void schedule(TimerTask task, Date time) {
        sched(task, time.getTime(), 0);
    }

 3.   // 延迟 delay 毫秒后,以间隔 period 为一个周期循环执行 task 任务
    public void schedule(TimerTask task, long delay, long period) {
        if (delay < 0)
            throw new IllegalArgumentException("Negative delay.");
        if (period <= 0)
            throw new IllegalArgumentException("Non-positive period.");
        sched(task, System.currentTimeMillis()+delay, -period);
    }

  4.   // 指定时间点 time 开始执行 task 任务,并且以 period 为周期循环执行
    public void schedule(TimerTask task, Date firstTime, long period) {
        if (period <= 0)
            throw new IllegalArgumentException("Non-positive period.");
        sched(task, firstTime.getTime(), -period);
    }

    
  5. // 对比3的区别在于AtFixedRate(以固定频率执行),在sched方法中传参period正值,而3传的是负
     // 值,正负值的区别涉及到TimerTask,参考下文
    public void scheduleAtFixedRate(TimerTask task, long delay, long period) {
        if (delay < 0)
            throw new IllegalArgumentException("Negative delay.");
        if (period <= 0)
            throw new IllegalArgumentException("Non-positive period.");
        sched(task, System.currentTimeMillis()+delay, period);
    }

 6. // 对比4的区别参考5和3
    public void scheduleAtFixedRate(TimerTask task, Date firstTime,
                                    long period) {
        if (period <= 0)
            throw new IllegalArgumentException("Non-positive period.");
        sched(task, firstTime.getTime(), period);
    }
// 这个方法的主要作用是完善任务信息,并将任务添加到任务队列中 
private void sched(TimerTask task, long time, long period) {
        if (time < 0)
            throw new IllegalArgumentException("Illegal execution time.");
       
        //  对period进行数值约束,防止后续用到period时出现数值溢出
        if (Math.abs(period) > (Long.MAX_VALUE >> 1))
            period >>= 1;

        synchronized(queue) {
            // 先判断任务线程thread是否还计划继续添加新任务
            if (!thread.newTasksMayBeScheduled)
                throw new IllegalStateException("Timer already cancelled.");
            
            synchronized(task.lock) {
                if (task.state != TimerTask.VIRGIN)
                    throw new IllegalStateException(
                        "Task already scheduled or cancelled");
                // 设置任务task的相关信息
                task.nextExecutionTime = time;
                task.period = period;
                // 将任务的状态表位计划中(已添加至任务队列)
                task.state = TimerTask.SCHEDULED;
            }
            // 进行添加操作
            queue.add(task);
            if (queue.getMin() == task)
                queue.notify();
        }
    }

3.2 任务取消

// 取消该定时器,清空任务队列。唤醒等待线程。
public void cancel() {
        synchronized(queue) {
            thread.newTasksMayBeScheduled = false;
            queue.clear();
            queue.notify();  // In case queue was already empty.
        }
}

3.3 任务清除

// 源码中无调用,是提供给外部使用的一个方法
public int purge() {
         int result = 0;

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

快速移除任务队列中所有被取消的任务,并重新堆化队列,返回删除的任务个数。Timer中并没有定义删除子任务
的方法。而唯一可以删除的形式,就是设定子任务状态,然后调用purge()方法进行一次洗牌。这种做法和JVM GC
中标记回收有点异曲同工之处。倘若将回收的方法,公开出来,则Timer内部需要提供很健壮的任务管理机制,防
止在高并发的情况下,队列维持的堆不会出现数据错误,或性能问题(想一下如果有大量的移除操作,那么每个移
除操作都需要同步队列,然后重新堆化)。

 


TimerThread

1. 成员变量

字面义是“能否添加新任务”,默认为true,该字段为ture且任务队列中已经不存在任务时,我们可以优雅的结束定时器。注意,这个字段受到队列对象监视器的保护,当它被设置为false时(可能是任务取消也可能是线程被意外杀死)意味着当前Timer已经不存在实例对象,所以定时器在执行任务时,需要确保其值为true 

boolean newTasksMayBeScheduled = true;

任务队列

  private TaskQueue queue;

2. 构造方法

TimerThread(TaskQueue queue) {
        this.queue = queue;
    }

3. 核心方法

    public void run() {
        try {
            mainLoop();
        } finally {
            // 如果线程意外终止,则同样需要走取消逻辑,清空队列,并标记此timer不再允许添加任务
            synchronized(queue) {
                newTasksMayBeScheduled = false;
                queue.clear();  // Eliminate obsolete references
            }
        }
    }

  private void mainLoop() {
        while (true) {
            try {
                TimerTask task;
                boolean taskFired;
                synchronized(queue) {
                    // Wait for queue to become non-empty
                    while (queue.isEmpty() && newTasksMayBeScheduled)
                        queue.wait();
                    if (queue.isEmpty())
                        break; // Queue is empty and will forever remain; die

                    // Queue nonempty; look at first evt and do the right thing
                    long currentTime, executionTime;
                    task = queue.getMin();
                    synchronized(task.lock) {
                        if (task.state == TimerTask.CANCELLED) {
                            queue.removeMin();
                            continue;  // No action required, poll queue again
                        }
                        currentTime = System.currentTimeMillis();
                        executionTime = task.nextExecutionTime;
                        if (taskFired = (executionTime<=currentTime)) {
                            if (task.period == 0) { // Non-repeating, remove
                                queue.removeMin();
                                task.state = TimerTask.EXECUTED;
                            } else { // Repeating task, reschedule
                                queue.rescheduleMin(
                                  task.period<0 ? currentTime   - task.period
                                                : executionTime + task.period);
                            }
                        }
                    }
                    if (!taskFired) // Task hasn't yet fired; wait
                        queue.wait(executionTime - currentTime);
                }
                if (taskFired)  // Task fired; run it, holding no locks
                    task.run();
            } catch(InterruptedException e) {
            }
        }
    }

TaskQueue 任务队列

1. 成员变量

优先级队列,它维护了一个最小堆结构(本文不具体介绍,可以简单理解为就是父节点都小于子节点的这样一棵树。而根节点就是下次运行时间最小的任务,注意,这个队列的起始下标为1,第一个任务被放在queue[1]的位置上,与我另一篇对堆结构的分析文章中有所不同,所以在算法逻辑上虽然无区别,但细节上有细微的差别。比如节点n的左右子节点为2n和2n+1,父节点为n>>1,如果不关注算法的话也可以不用细究)

 private TimerTask[] queue = new TimerTask[128];

任务的实际数量

 private int size = 0;

2. 主要方法

2.1 添加任务

    void add(TimerTask task) {
        // 扩容机制
        if (size + 1 == queue.length)
            queue = Arrays.copyOf(queue, 2*queue.length);
        // 注意,这里保证了第一个任务被添加时是放在下标1的位置上,下标0是不存放task的
        queue[++size] = task;
        // 确保将此任务按顺序放在合适的位置
        fixUp(size);
    }

2.2 获取优先级最高的任务

也就是下次预期执行时间最早的,在queue中排在首位的(下标0不存放task,优先级最高的是1)

   TimerTask getMin() {
        return queue[1];
    }

2.3 重新设定下一次执行时间

    void rescheduleMin(long newTime) {
        queue[1].nextExecutionTime = newTime;
        fixDown(1);
    }

2.4 调整堆结构

    //  以向上调整的方式调整顺序,以维持堆结构
    private void fixUp(int k) {
        while (k > 1) {  
            // 父节点j
            int j = k >> 1;
            // 判断是否需要交换
            if (queue[j].nextExecutionTime <= queue[k].nextExecutionTime)
                break;
            // 如果发生了交换,继续循环,以确保交换后以原先父节点的位置为子节点的结构也满足堆结构
            TimerTask tmp = queue[j];  queue[j] = queue[k]; queue[k] = tmp;
            k = j;
        }
    }

    // 以向下调整的方式调整顺序,以维持堆结构
    private void fixDown(int k) {
        int j;
        // 子节点j
        while ((j = k << 1) <= size && j > 0) {
            // 比较左右子节点,选出最小的
            if (j < size &&
                queue[j].nextExecutionTime > queue[j+1].nextExecutionTime)
                j++; // j indexes smallest kid
            // 跟父节点k进行比较,如果k较小,不需要交换,停止循环
            if (queue[k].nextExecutionTime <= queue[j].nextExecutionTime)
                break;
            // 如果进行了交换,则需要确保交换后,以原先子节点为父节点的结构也满足堆结构,继续循环直至完全满足
            TimerTask tmp = queue[j];  queue[j] = queue[k]; queue[k] = tmp;
            k = j;
        }
    }

2.5 重新堆化

 // 堆化  
 void heapify() {
        // 从最后一个父节点开始,遍历所有父节点,执行向下调整操作,以重新构建堆结构(在timer.purge中被调用,即堆中元素发生了大量删除时需要重新构建堆)
        for (int i = size/2; i >= 1; i--)
            fixDown(i);
    }

TimerTask 定时任务

public abstract class TimerTask implements Runnable {...}

这个类是一个继承自接口Runnable的抽象类,需要实现类自己去补充run方法,也就是自定义任务内容。

这个类比较简单,简单介绍一下

// 用于内部保证同步逻辑的一个锁变量
final Object lock = new Object();

// 状态变量,,初始状态为virgin。只有这个状态的任务才可以添加到queue中,子任务添加后,会改变子任务
// 的状态,所以子任务不会被反复多次添加到queue中
int state = VIRGIN;

static final int VIRGIN = 0;  // 初始化
static final int SCHEDULED = 1; // 任务被添加到queue中即会设置该状态
static final int EXECUTED = 2; // 被执行过,只有不反复循环的子任务会被设置该状态
static final int CANCELLED = 3; // 被取消

// 下次被执行的时间(维持最小堆的判断标准)
long nextExecutionTime;

// 周期,初始是0毫秒,即不被反复执行。注意,该值区分正负,负数表示,每执行完一次后,间隔指定时间再次
// 执行。正数,表示每次开始执行后,指定间隔再次执行,前者用的比较多。
// nextExecutionTime = task.period<0 ? currentTime - task.period : executionTime + task.period
long period = 0;
 // 取消任务  
 public boolean cancel() {
        synchronized(lock) {
            boolean result = (state == SCHEDULED);
            state = CANCELLED;
            return result;
        }
    }
    
  // 获取任务的下一次计划执行时间
  public long scheduledExecutionTime() {
        synchronized(lock) {
            return (period < 0 ? nextExecutionTime + period
                               : nextExecutionTime - period);
        }
    }


上面两个方法在JDK源码中都没有被调用,是供外部调用的。

此部分转载于https://www.cnblogs.com/jilodream/p/5887110.html

最后的最后,来谈谈Timer类的定位:

(1)前Timer时代。

Timer是jdk1.3的时候,添加进源码的。这个时候大概是2000年左右。具体java被推出,才仅仅过去5年,所以1.3的主要改进,表现在新增的大量类库上。而在此之前,想拥有一个如Timer般的定时功能,是非常麻烦的,基本都要手动去实现。

(2)后Timer时代

查看了Timer的源代码之后,我们发现Timer在使用中存在这么问题:

1、定时任务是顺序执行的,也就是说后续的任务,一定要等到前边的任务执行完毕后,才会执行,否则将会一直等待。(其实这一点说不上来好还是坏,因为有时候我们可能会希望尽管是定时任务,但是执行时是有顺序完成和开始的,是要保证先后顺序的)

2、对系统时间非常敏感,通过代码我们知道,在每次子任务被取出后(执行run前),都会计算一遍执行时间,同时在判定子任务的执行时间是否已经到来时,都是直接获取到系统时间。倘若系统时间发生了修改,而使用的计划时间仍然是使用上次修改前的时间段时,就会出现一些意想不到的结果。如计划是5秒后执行,主线程wait 5秒钟后,被唤醒,在这5秒钟内,系统时间向后推迟了1天,那么主任务,仍然会执行该子任务(其他的也都会依次迅速执行,因为时间已经过了)。而倘若向前调整一天,那么主线程判断的时间仍然是,调整时间前的时间点,所以需要再等待一天。因此会出(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )现很多人以为Timer在调整时间后,被挂起,但是查看线程状态,发现还存在的奇怪场景。

3、子任务之间存在依赖。其实子任务之间的依赖关系并不强,无非就是前边的子任务执行完后,后边的子任务才可以开始执行。但是倘若在执行某个子任务时,捕捉到了异常,那么线程会立刻结束执行,后续的子任务都不会执行了,这个问题有时会对我们造成很大的困扰。

为了解决以上种种在jdk1.5中提供了ScheduledExecutorService接口以供开发者使用。

这个接口的实现,主要是通过线程池的形式,解决了上述遇到的问题(线程池也是jdk 1.5时才推出的),很多人因此认为Timer已经过时了,我觉得完全没有必要这样认为,通过自己对比Timer的原理和ScheduledExecutorService的改进之后。我们发现很多地方Timer仍然是有自己存在的必要的,只是占用场景不如ScheduledExecutorService多罢了。关于ScheduledExecutorService的学习,此处不再罗列,有兴趣的同学可以自己学习。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值