Timer类jdk文档翻译及源码分析

来源:互联网 发布:解救吾先生知乎 编辑:程序博客网 时间:2024/06/16 15:23

Timer是进行任务调度的工具类,用来执行任务。Task可以被执行一次,也可以按照一个固定的时间间隔重复执行。

每个定时器对象都维护一个single后台线程,它用于顺序执行该定时器对象的所有任务。因此提交给Timer对象的任务应当能够快速执行完。如果一个任务耗时太长,有可能导致后续任务延迟执行,或者导致很多个后续任务积压,并且快速的串行执行完成。

当对Timer对象的最后一个引用消失,并且所有未完成的任务都已经完成执行时,执行task的thread将优雅的终止(terminates gracefully)并且成为gc的目标。但是,如果想等待所有任务执行完成,可能会等待很长时间。默认情况下,执行task的thread(the task execution thread)不作为守护线程(daemon thread),因此它会阻止应用程序关闭。如果调用者想要快速terminate Timer的任务执行线程,应该调用Timer类的取消方法。

如果调用了Timer类的cancel方法,接下来任何对任务进行调度的attempt都会导致一个IllegalStateException。

Timer类是线程安全的,在没有额外同步措施的情况下,多个线程可以共享一个Timer对象。

Timer类不保证任务被调度的实时性:它基于Object.wait(long)方法进行调度任务。

jdk5引入了concurrent包,其中有一个并发工具类就是ScheduledThreadPoolExecutor,该线程池提供了一种在给定速率下货延迟 重复执行任务的能力。它是Timer/TimerTask的一个更好的替代品,因为它提供了多个线程执行任务,并且接受不同的时间单位(time unit),并且不需要接收一个TimerTask子类(只需要实现Runnable的任务即可)。
将ScheduledThreadPoolExecutor配置成一个线程将等价于Timer类。

Timer类可以适用于大量并发任务的执行(数千个任务应该没有问题)。在该类内部,它使用了一个binary heap代表他的task queue,因此调度一个task的时间复杂度是O(log n)。(这是从调度最小堆的时间复杂度来分析,实际上如果一个timer对象同时处理数千个任务,那么延迟可能会非常大)

所有该类的构造函数都会启动一个timer thread。

Timer类有三个缺陷:

  1. 如上文所说,每个timer对象仅启动一个线程执行任务,如果遇到耗时较长的任务,有可能造成后续任务延迟,或大量任务积压。
  2. Timer类对调度的支持是基于绝对时间而不是相对时间,这意味着任务对于系统时钟的改变是敏感的。
  3. Timer的另一个问题在于,如果TimerTask抛出未检查的异常,Timer线程并不捕获异常,所以TimerTask抛出的未检查的异常会终止timer线程。这种情况下,Timer也不会再重新恢复线程的执行了;它错误的认为整个Timer都被取消了。此时,已经被安排但尚未执行的TimerTask永远不会再执行了,新的任务也不能被调度了:
public void run() {    try {        mainLoop();//一旦task抛出InterruptedException之外的Exception,mainLoop()方法将退出,向上抛出异常,导致执行finally块,结束thread。    } finally {        // Someone killed this Thread, behave as if Timer cancelled        synchronized(queue) {            newTasksMayBeScheduled = false;            queue.clear();  // Eliminate obsolete references        }    }}

下面重点说一下任务队列这个数据结构。

Timer类维护了一个最小堆的任务队列,底层为数组,根据提交的任务的executeTime来维护队列中任务的顺序,最近需要执行的任务在堆上方,即最小堆(最小堆保证每个节点均小与等于其左子节点和右子节点)。

对于Timer类,在任务队列上的操作主要为:add一个任务、take一个min任务并remove。相对于直接使用有序数组存储队列来说,维护一个最小堆,其add和remove的时间复杂度由O(n)下降为O(logn)。

TaskQueue的成员变量如下:

class TaskQueue {    private TimerTask[] queue = new TimerTask[128];    /**     * The number of tasks in the priority queue.  (The tasks are stored in     * queue[1] up to queue[size]).     */    private int size = 0;

为了维护最小堆时移位方便,队列选择从index为1的位置开始存储元素。

add方法如下:

/** * Adds a new task to the priority queue. */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);}

如果堆满了,则进行扩容(2倍),之后把任务放到堆底,然后调整堆结构。fixUp方法如下:

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

调整最小堆结构的方法为:比较新增节点与其父节点大小,如果新增节点比较小,则交换两个节点位置,否则结束循环;之后递归的进行判断,直到递归到堆顶为止。

队列提供的另一个方法为removeMin,即取出最小堆堆顶元素并且remove掉:

/** * Remove the head task from the priority queue. */void removeMin() {    queue[1] = queue[size];    queue[size--] = null;  // Drop extra reference to prevent memory leak    fixDown(1);}

removeMin的做法为,把堆底元素放到堆顶,然后调整堆结构:

private void fixDown(int k) {    int j;    while ((j = k << 1) <= size && j > 0) {        if (j < size &&            queue[j].nextExecutionTime > queue[j+1].nextExecutionTime)            j++; // j indexes smallest kid        if (queue[k].nextExecutionTime <= queue[j].nextExecutionTime)            break;        TimerTask tmp = queue[j];  queue[j] = queue[k]; queue[k] = tmp;        k = j;    }}

首先将k左移位,得到它的左子节点,将j指向左子节点和右子节点较小的那个,比较k与j为止上任务的大小,如果k比较小,说明此时堆已有序,结束循环,否则交换k与j位置上的任务,继续递归j。

通过fixUp和fixDown方法,保证了每次对最小堆add和remove方法时保持最小堆的特性。

接下来看一下Timer类是怎样调度任务进行执行的。

每次创建一个Timer对象,其构造方法中就会启动一个线程,从TaskQueue中获取任务执行:

public Timer() {    this("Timer-" + serialNumber());}
public Timer(String name) {    thread.setName(name);    thread.start();}

thread是一个TimerThread类,继承了Thread类,其run方法如下:

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()方法如下:

/** * The main timer loop.  (See class comment.) */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) {        }    }}

mainLoop()方法是一个死循环,用来不停的从TaskQueue中获取任务进行执行。执行任务时,首先获取queue的对象锁,判断queue是否为空,如果是则进行wait释放queue的对象锁,进入queue对象锁的阻塞队列中进行休眠。否则取出队列堆顶任务,判断其执行时间是否小于等于当前时间,如果不是,则继续wait(executionTime - currentTime)一段时间再醒来执行;否则判断任务的执行周期period,如果是0,说明任务只执行一次,此时将其从队列中移除,并且将状态标记为EXECUTED;否则该任务属于定时执行,则更新其在队列中的executionTime,然后重新调整堆结构。最后调用TimerTask的run()方法进行执行任务。注意,此处直接调用的是Thread子类的run()方法而非start()方法,这意味着不会启动新的线程来执行任务。

接下来看一下Timer类提交任务的方法:

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

注意到两个方法大致相同,接收一个任务,一个任务延迟时间,一个任务开始执行后的执行固定间隔。不过在调用内部sched方法传递period时,schedule方法传递的是负数,而scheduleAtFixedRate传递的是正数。这是为什么呢?

翻回头看一下上面mainLoop()方法,在处理堆顶period非0的任务时,此任务为按照固定间隔持续执行,需要把该任务的executeTime进行调整,然后调整堆结构:

queue.rescheduleMin(task.period<0 ? currentTime - task.period : executionTime + task.period);

可以看到,如果period小于0,那么下一次的执行时间为当前时间+period;如果period大于0,那么下一次的执行时间为executionTime+period。可是这有什么区别呢,难道currentTime和executionTime不相同吗?

确实不太相同,executionTime是小于等于currentTime的(看mainLoop,因为已经进了if判断,所以executionTime<=currentTime一定成立),可能由于gc线程或其他线程影响,占据了较多cpu时间片,或者之前遇到了一个耗时较长的任务,导致timer任务执行线程执行当前任务有延迟,使executionTime < currentTime。

假设某一时间点currentTime为10,executionTime为6(因为一些原因导致currentTime与executionTime有较大差异),period为3,那么使用schedule提交的方法由于period为负数,以后的executionTime应为13 16 19 22… 使用scheduleAtFixedRate提交的方法,以后的executionTime则为 9 12 15 18。可以看到,schedule提交的任务能保证重复任务执行间隔永远为period(理论上),而scheduleAtFixedRate提交的任务在某些出现延迟的情况下会连续执行多个任务。也就是说,schedule基于实际执行时间,而scheduleAtFixedRate基于理论执行时间。

这会导致什么问题呢,这会导致:schedule提交的任务保证了重复任务之间的间隔性,却有可能导致在一个时间段内任务被少执行了(由于延迟);scheduleAtFixedRate提交的任务能够保证在一个时间段内任务执行的次数,却不能保证任务之间的间隔频率。

因此在使用Timer类时,需要仔细考虑业务场景,选择合适的方法进行任务提交。

原创粉丝点击