码迷,mamicode.com
首页 > 编程语言 > 详细

细说java.util.Timer

时间:2015-08-29 06:23:40      阅读:310      评论:0      收藏:0      [点我收藏+]

标签:timer   timer原理   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). 

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

但是在测试时却发现,在执行完一个一次性任务后,任务线程却不终止,而是一直阻塞,这不知道是何原因,使用的JDK是1.7.0_79。

使用jstack查看到这个线程的状态如下:

"Timer-0" prio=5 tid=0x00007ff184046000 nid=0x5807 in Object.wait() [0x000000011ebea000]
   java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000007ab1bbea0> (a java.util.TaskQueue)
at java.lang.Object.wait(Object.java:503)
at java.util.TimerThread.mainLoop(Timer.java:526)
- locked <0x00000007ab1bbea0> (a java.util.TaskQueue)
at java.util.TimerThread.run(Timer.java:505)

可以看到Timer任务线程处理阻塞状态。

一个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。

版权声明:本文为博主原创文章,未经博主允许不得转载。

细说java.util.Timer

标签:timer   timer原理   timer缺陷   

原文地址:http://blog.csdn.net/mhmyqn/article/details/48070879

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!