细说java.util.Timer

Timer是用于管理在后台执行的延迟任务或周期性任务,其中的任务使用java.util.TimerTask表示。任务的执行方式有两种:

  • 按固定速率执行:即scheduleAtFixedRate的两个重载方法
  • 按固定延迟执行:即schedule的4个重载方法

具体差别会在后面详细说明。

我们要实现一个定时任务,只需要实现TimerTask的run方法即可。每一个任务都有下一次执行时间nextExecutionTime(毫秒),如果是周期性的任务,那么每次执行都会更新这个时间为下一次的执行时间,当nextExecutionTime小于当前时间时,都会执行它。

一、使用方式

Timer的具体使用方法非常简单,比如:

        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("Timer is running");
            }
        }, 2000);
上面这个定时任务表示在2秒后开始执行,只执行一次。当然还可以执行周期性任务,只需要添加schedule的第三个参数period,如:

        Timer timer = new Timer();
        timer. scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println("Timer is running");
            }
        }, 2000, 5000);
表示2秒后开始执行,然后每隔5秒执行一次。

二、具体实现的分析

对于每一个Timer,后台只使用一个线程来执行所有的任务。而所有的任务都保存到一个任务队列java.util.TaskQueue中,它是Timer的一个内部类,这是一个优先级队列,使用的算法是最小二叉堆(binary min heap)。

Timer的文档中说:

After the last live reference to a Timer object goes away and all outstanding tasks have completed execution, the timer's task execution thread terminates gracefully (and becomes subject to garbage collection).  However, this can take arbitrarily long to occur.

意思是(没有完全按照原文翻译):当任务队列中所有的任务都执行完后,即没有一次性执行的任务,也没有周期性的任务,那么Timer的后台线程将优雅地终止,并成为垃圾回收的对象。但是这可能要很长时间后才发生。

这里虽说是要终止线程,但是时间不确定,有可能永远不会终止,在执行完任务后线程处于WAITING状态,直到虚拟机退出。

一个TimerTask有四种状态:

  • VIRGIN:新创建的任务的状态,表示还未安排执行
  • SCHEDULED:已安排执行,对于非周期性的任务来说,表示还未执行,当把任务添加到任务执行队列时的状态,即调用Timer.schedule时
  • EXECUTED:表示非周期性任务已经执行或正在执行中,并且还未被取消
  • CANCELLED:表示任务已经取消,当调用cancel方法后即为该状态,该状态的任务会在每次执行时被移出队列

下面看看当调用Timer.schedule时发生了什么,所有调用Timer.schedule都是调用的一个私有方法sched。

比如延迟执行的任务(非周期性任务):

    public void schedule(TimerTask task, long delay) {
        // 延迟不能小于0
        if (delay < 0)
            throw new IllegalArgumentException("Negative delay.”);
        // 调用私有的sched方法,注意这里的执行时间是绝对时间,而period为0表示不是周期性任务
        sched(task, System.currentTimeMillis()+delay, 0);
    }

    private void sched(TimerTask task, long time, long period) {
        if (time < 0)
            throw new IllegalArgumentException("Illegal execution time.");

        // Constrain value of period sufficiently to prevent numeric
        // overflow while still being effectively infinitely large.
        if (Math.abs(period) > (Long.MAX_VALUE >> 1))
            period >>= 1;

        synchronized(queue) {
            // 如果定时器已经取消,那么不能再执行任何任务,所以抛出异常
            if (!thread.newTasksMayBeScheduled)
                throw new IllegalStateException("Timer already cancelled.");

            synchronized(task.lock) {
                // 如果要执行的任务状态不是VIRGIN,那么抛出异常
                if (task.state != TimerTask.VIRGIN)
                    throw new IllegalStateException(
                        "Task already scheduled or cancelled”);
                // 设置任务的执行时间,注意这个时间是绝对时间
                task.nextExecutionTime = time;
                // 设置任务执行的周期
                task.period = period;
                // 设置任务的状态为SCHEDULED
                task.state = TimerTask.SCHEDULED;
            }
            // 把任务添加到任务队列,等任务执行线程调试执行
            queue.add(task);
            // 检查下一个将要执行的任务是不是当前添加的任务,如果是则通知后台任务线程
            if (queue.getMin() == task)
                queue.notify();
        }
    }
执行任务的后台线程,Timer使用了一个内部类TimerThread,它有一个newTasksMayBeScheduled属性表示是否还能执行任务,默认为true,当调用cancel方法时设置为false,之后将不能再添加或执行任务。同时它还持有一个任务队列的引用,而不是直接引用Timer的任务队列,这是因为为了防止循环引用导致Timer无法被垃圾回收,以及任务线程不能正常终止。

下面看一下这个任务线程的实现:

    public void run() {
        try {
            mainLoop();
        } finally {
            // Someone killed this Thread, behave as if Timer cancelled
            synchronized(queue) {
                newTasksMayBeScheduled = false;
                queue.clear();  // Eliminate obsolete references
            }
        }
    }

实际上是调用的mainLoop方法:

    private void mainLoop() {
        while (true) {
            try {
                TimerTask task;
                boolean taskFired;
                synchronized(queue) {
                    // 等待任务队列变成非空,如果任务队列为空并且Timer未取消,就阻塞线程,等待任务队列为非空,从前面可以看到,每次添加完一个任务后,会通知这个线程来检查执行相应的任务
                    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)) {
                        // 如果不是非周期性任务,那么从任务队列中移除该任务,并把该任务的状态设置为EXECUTED
                            if (task.period == 0) { // Non-repeating, remove
                                queue.removeMin();
                                task.state = TimerTask.EXECUTED;
                            } else { // Repeating task, reschedule
                                // 如果是周期性任务,则重新设置任务的下一次执行时间,这里要注意period有可能为正和负
                                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) {
            }
        }
    }

其中重新设置任务的下一次执行时间的逻辑:

queue.rescheduleMin(task.period<0 ? currentTime   - task.period : executionTime + task.period);
这里任务执行周期可为正和负:

  • 正数:表示按照固定的速率调度执行,比如执行周期是每5秒执行一次,如上一次执行时间是20:51:30,那么下一次执行时间就为20:51:35,如果由于执行其他任务的时间超过5秒,比如用了15秒,那么这有可能会导致这种任务不能够在指定的时间执行,这就破坏了任务执行的速率(rate),但是会在后面连续执行3次。
  • 负数:表示按照固定的延迟来调度执行,比如执行周期是每5秒执行一次,在正常情况下,如它的执行时间是20:51:30,但是由于执行其他任务时间花了8秒秒,即执行到当前任务时是20:51:38,那么它的下一次执行时间将向后推迟,即20:51:43。

三、Timer的缺陷

1、由于执行任务的线程只有一个,所以如果某个任务的执行时间过长,那么将破坏其他任务的定时精确性。如一个任务每1秒执行一次,而另一个任务执行一次需要5秒,那么如果是固定速率的任务,那么会在5秒这个任务执行完成后连续执行5次,而固定延迟的任务将丢失4次执行。

2、如果执行某个任务过程中抛出了异常,那么执行线程将会终止,导致Timer中的其他任务也不能再执行。

3、Timer使用的是绝对时间,即是某个时间点,所以它执行依赖系统的时间,如果系统时间修改了的话,将导致任务可能不会被执行。

四、更好的替代方法

由于Timer存在上面说的这些缺陷,在JDK1.5中,我们可以使用ScheduledThreadPoolExecutor来代替它,使用Executors.newScheduledThreadPool工厂方法或使用ScheduledThreadPoolExecutor的构造函数来创建定时任务,它是基于线程池的实现,不会存在Timer存在的上述问题,当线程数量为1时,它相当于Timer。

猜你喜欢

转载自blog.csdn.net/mhmyqn/article/details/48070879