Java定时任务之Timer & TimerTask

Timer & TimerTask 作为java的任务调度器之一,了解其内部原理有助于我们更好的理解它与其他调度器之间的异同。

  • TimerTask
  • Timer及其调度过程
  • 总结

TimerTask

TimerTask作为被调度任务的抽象类,继承于Thread,但是没有实现run方法,而是留给我们根据具体业务需求来实现。

1. TimerTask主要函数

  • [x] public boolean cancel();
  • [x] public long scheduledExecutionTime();

cancel()

/**
*设置一个任务的状态位为cancelled,但是并没有从任务队列中删除。
*而是继续存在于任务队列,调用purge();方法后会将无效的任务(cancelled)删除。
**/
 public boolean cancel() {
        synchronized(lock) {
            boolean result = (state == SCHEDULED);
            state = CANCELLED;
            return result;
        }
    }

scheduledExecutionTime()

/**
*任务最近一次将要被执行的时间。
*period之所以可以小于0,因为在Timer中设置调度方式式时有两种方案。
**/
 public long scheduledExecutionTime() {
        synchronized(lock) {
            return (period < 0 ? nextExecutionTime + period
                               : nextExecutionTime - period);
        }
    }

Timer

1.Timer的调度方法

• [ ] schedule(TimerTask task, long delay);
• [ ] schedule(TimerTask task, Date time);
• [ ] schedule(TimerTask task, long delay, long period);
• [ ] schedule(TimerTask task, Date firstTime, long period);
• [ ] scheduleAtFixedRate(TimerTask task, long delay, long period);
• [ ] scheduleAtFixedRate(TimerTask task, Date firstTime, long period);
• [x] sched(TimerTask task, long time, long period);

schedule(TimerTask task, long delay): 当前系统时间延迟delay后执行,只执行一次。

schedule(TimerTask task, Date time): 在指定的Date执行,只执行一次。

schedule(TimerTask task, long delay, long period): 当前系统时间延迟delay执行,后续每隔period执行一次,循环执行。

schedule(TimerTask task, Date firstTime, long period): 第一次执行的时间为Date,后续每隔period执行一次,循环执行。

scheduleAtFixedRate(TimerTask task, long delay, long period): 同样是当前系统时间延迟delay后执行,每隔period执行一次,循环执行。但是在计算下次执行时间上与schedule()有所不同,比如最近一次执行时间是X,period为5,Timer调度存在3毫秒的延迟,那么由schedule计算出来的下次执行时间为X+5+3,而由scheduleAtFixedRate计算出来的时间仍为X+period,这个下次执行时间会影响Task在任务队列中的位置。

scheduleAtFixedRate(TimerTask task, Date firstTime, long period): 理解同上。

sched(TimerTask task, long time, long period): 该函数是以上所有调度方式的内部执行函数,接下来会详细介绍。

2.初始化

/**
* TaskQueue queue = new TaskQueue();
* TaskQueue是Timer的一个内部类,其主要成员为:TimerTask[] queue = new TimerTask[128];
* 以及提供对该数组的一些操作。
*
**/
 private void sched(TimerTask task, long time, long period) {
        if (time < 0)
            throw new IllegalArgumentException("Illegal execution time.");
        if (Math.abs(period) > (Long.MAX_VALUE >> 1))
            period >>= 1;

        synchronized(queue) {
            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.nextExecutionTime = time;
                task.period = period;
                task.state = TimerTask.SCHEDULED;
            }

            queue.add(task);
            if (queue.getMin() == task)
                queue.notify();
        }
    }

该函数的意义是用户提交一个任务后,设置好下次执行的时间,时间片(period)大小,任务状态后,加入任务队列,实质是放入一个任务数组。

//实现细节
void add(TimerTask task) {
        // Grow backing store if necessary
        if (size + 1 == queue.length)
            queue = Arrays.copyOf(queue, 2*queue.length);

        queue[++size] = task;
        fixUp(size);
    }


private void fixUp(int k) {
        while (k > 1) {
            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;
        }
    }

这里解释下fixUp(),它其实完成的是一个排序功能,但是不是绝对意义上的有序,用相对有序更为恰当,它的目的只是将新的任务放到一个合适的位置。
k>>1相当于k/2,这样处理的好处是提高对于大量堆积任务的排序性能,因为他极大的减少了需要排序的次数,同时也保证了一定程度的有序性。试想,每次有新的任务进来时,都要完成一次绝对排序,对于整体性能影响是比较大的。

3.调度过程

Timer实际被调用的两个构造函数

private final TimerThread thread = new TimerThread(queue);

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

 public Timer(String name, boolean isDaemon) {
    thread.setName(name);
    thread.setDaemon(isDaemon);
    thread.start();
}

TimerThread是Timer的一个内部类,继承Thread,是Timer中真正执行调度动作的线程。但是需要注意的是他是单线程,而非线程池,也就是说,Timer对于任务是串行执行的。

接下来看下TimerThread的run方法

public void run() {
        try {
            mainLoop();
        } finally {
            synchronized(queue) {
                newTasksMayBeScheduled = false;
                queue.clear();  
            }
        }
    }

private void mainLoop() {
        while (true) {
            try {
                TimerTask task;
                boolean taskFired;
                synchronized(queue) {
                    while (queue.isEmpty() && newTasksMayBeScheduled)
                        queue.wait();
                    if (queue.isEmpty())
                        break; 
                    long currentTime, executionTime;
                    task = queue.getMin();
                    synchronized(task.lock) {
                        if (task.state == TimerTask.CANCELLED) {
                            queue.removeMin();
                            continue;  
                        }
                        currentTime = System.currentTimeMillis();
                        executionTime = task.nextExecutionTime;
                        if (taskFired = (executionTime<=currentTime)) {
                            if (task.period == 0) { 
                                queue.removeMin();
                                task.state = TimerTask.EXECUTED;
                            } else {
                                queue.rescheduleMin(
                                  task.period<0 ? currentTime   - task.period
                                                : executionTime + task.period);
                            }
                        }
                    }
                    if (!taskFired) 
                        queue.wait(executionTime - currentTime);
                }
                if (taskFired)  
                    task.run();
            } catch(InterruptedException e) {
            }
        }
    }
}

由上可见,主要的任务轮询发生在mainloopd的while(true)循环中。
首先进行 while (queue.isEmpty() && newTasksMayBeScheduled)循环,第一次运行时,queue必然为空,且newTasksMayBeScheduled初始值为true。所以线程第一次执行时一定会走到 queue.wait();这里,它会因为任务队列为空而进入等待,相应的,我们可以看到在sched()中提交任务后会有notify()动作来激活该线程。
那么继续往下看,为何在激活后又需要判断队列是否为空呢?
我们看一段代码

public void cancel() {
        synchronized(queue) {
            thread.newTasksMayBeScheduled = false;
            queue.clear();
            queue.notify();  // In case queue was already empty.
        }
    }

Timer的cancle()操作,同样会激活线程,所以需要再次检查队列是否为空。而且需要注意的是,再次检查,如果队列为空,则跳出循环,也就是说TimerThread执行结束,调度完毕。
若队列不为空,进行queue.getMin()操作,可以认为是取了TimerTask[]数组中的”task head“元素。
检查取到的任务的状态,是否为cancel,若是则从队列中删除,继续循环部分。
之前提到TimerTask中的cancel()只是更改了标志位,实际的移除在是调用removeMin()来删除的。
接下来判断该任务是否满足执行条件。

currentTime = System.currentTimeMillis();
executionTime = task.nextExecutionTime;
                        if (taskFired = (executionTime<=currentTime)) {
                            if (task.period == 0) { 
                                queue.removeMin();
                                task.state = TimerTask.EXECUTED;
                            } else { 
                                queue.rescheduleMin(
                                  task.period<0 ? currentTime   - task.period
                                                : executionTime + task.period);
                            }
                        }

意思是取下次执行时间与系统当前时间做比较,如果下次执行时间晚于系统当前时间,则说明该任务不满足执行条件。
反正满足执行条件,对于满足执行条件的任务,需要判断其时间片period的大小,如果是0,说明任务只需要执行一次。反正任务是循环任务。
对于period为0的任务,会将其从任务队列中移除,并将任务的标志位置为已执行。
对于需要循环执行的任务,会计算出下一次任务将要执行的时间并对相应字段进行设置,之前提到的调度方式有中的schedule与scheduleAtFixedRate的差别在这里也被体现出来,就参数而言,schedule中的period为大于0,scheduleAtFixedRate中的period小于0。
最后,对于满足执行条件的任务调用其TimerTask的run方法。
对于不满足执行条件的任务进行定时等待,等待时间为executionTime - currentTime。

总结

1.Timer & TimerTask的基本原理
2.Timer的缺点,它始终是单线程的,所以在处理任务时,可能会因为一个长时间的任务而导致其他任务无法被调度。
3.Timer中对于任务队列的处理方式(相对有序等思想)