美文网首页Java学习笔记
31-Timer的缺陷分析

31-Timer的缺陷分析

作者: 史路比 | 来源:发表于2018-02-04 22:30 被阅读76次

Timer的缺陷分析

Timer计时器可以定时(指定时间执行任务)、延迟(延迟5秒执行任务)、周期性地执行任务(每隔个1秒执行任务),但是,Timer存在一些缺陷:

  1. Timer在执行所有定时任务时只会创建一个线程。如果某个任务的执行时间长度大于其周期时间长度,那么就会导致这一次的任务还在执行,而下一个周期的任务已经需要开始执行了,当然在一个线程内这两个任务只能顺序执行,有两种情况:对于之前需要执行但还没有执行的任务,一是当前任务执行完马上执行那些任务(按顺序来),二是干脆把那些任务丢掉,不去执行它们。至于具体采取哪种做法,需要看是调用schedule还是scheduleAtFixedRate。

  2. 如果TimerTask抛出了一个未检查的异常,那么Timer线程就会被终止掉,之前已经被调度但尚未执行的TimerTask就不会再执行了,新的任务也不能被调度了。

  3. Timer支持基于绝对时间而不是相对时间的调度机制,因此任务的执行对系统时钟变化很敏感。

源码分析

下面通过分析Timer的源码来解释为什么会有上面的问题。

Timer的实现原理很简单,概括的说就是:Timer有两个内部类,TaskQueue和TimerThread,TaskQueue其实就是一个最小堆(按TimerTask下一个任务执行时间点先后排序),它存放该Timer的所有TimerTask,而TimerThread就是Timer新开的检查兼执行线程,在run中用一个死循环不断检查是否有任务需要开始执行了,有就执行它(注意:任务还是在这个线程执行)。

因此Timer实现的关键就是调度方法,也就是TimerThread的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方法中实现:

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

从第14行开始,这里取出那个最先需要执行的TimerTask,然后22行判断executionTime<=currentTime,其中executionTime就是该TimerTask下一个周期任务执行的时间点,currentTime为当前时间点,如果为true说明该任务需要执行了(注意可能是一个过时任务,应该在过去某个时间点开始执行,但由于某种原因还没有执行),接着第23行判断task.period == 0,Timer中period默认为0,表示该TimerTask只会执行一次,不会周期性地不断执行,所以为true就移除掉该TimerTask,然后待会会执行该TimerTask一次。如果task.period不为0,那就分为小于0和大于0,如果调用的是schedule方法:

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

那么period就小于0,如果调用的是scheduleAtFixedRate方法:

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

那么period就大于0。

回到mainLoop方法中,当period<0时,当前TimerTask下一次开始执行任务的时间就会被设置为currentTime - task.period,可理解为定时任务被重置,从现在开始,period周期间隔(那么之前预想在这个间隔内存在的任务执行就没了)后执行第一次任务,这种情况就是Timer的任务可能丢失问题。当period>0,当前TimerTask下一次开始执行任务的时间就会被设置为executionTime + task.period,即下一次任务还是按原来的算,因此如果这时executionTime + task.period还先于currentTime,那么下一个任务就会马上执行,也就是Timer的任务快速调用问题。

以上分析解释了第一点,下面解释第二点。
从代码上可以看到在死循环中只catch了一个InterruptedException,也就是当前线程被中断,因此Timer的线程是可以执行一段时间,然后被操作系统挂到一边休息,然后又回来继续执行的。但如果抛出其它异常,那么整个循环就挂掉,当然外层的run方法也没有catch任何异常:

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

这时就会造成线程泄露,同时之前已经被调度但尚未执行的TimerTask就不会再执行了,新的任务也不能被调度了。

补充:对于上面说到的Timer线程执行到一半被挂到一边去,这种情况与任务执行时间过长类似,如果调用schedule方法的话就有可能导致任务丢失。在Android中,有一种叫长连接的东西,它需要客户端发心跳包确保连接的存在,如果使用Timer实现定时发心跳包就可能会有问题,如果Timer线程在执行过程中被换出去了,那么调用schedule的就很有可能导致心跳包没有发出去,而调用scheduleAtFixedRate又可能会导致Timer线程没有占用CPU时心跳包没发出去,某一时刻又快速地发送好几个心跳包。因此在Android中一般使用AlarmManager实现心跳包的定时发送。

例子

下面举几个例子,演示一下Timer的缺陷。

问题一

Timer在执行定时任务时只会创建一个线程,如果存在多个任务,若其中某个任务因为某种原因而导致任务执行时间过长,超过了两个任务的间隔时间,会发生一些缺陷:

public class TimerTest04 {
    private Timer timer;
    public long start;

    public TimerTest04() {
        this.timer = new Timer();
        start = System.currentTimeMillis();
    }

    public void timerOne() {
        timer.schedule(new TimerTask() {
            public void run() {
                System.out.println("timerOne invoked ,the time:" + (System.currentTimeMillis() - start));
                try {
                    Thread.sleep(4000); // 线程休眠4000
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, 1000);
    }

    public void timerTwo() {
        timer.schedule(new TimerTask() {
            public void run() {
                System.out.println("timerOne invoked ,the time:" + (System.currentTimeMillis() - start));
            }
        }, 3000);
    }

    public static void main(String[] args) throws Exception {
        TimerTest04 test = new TimerTest04();

        test.timerOne();
        test.timerTwo();
    }
}

按照我们正常思路,timerTwo应该是在3s后执行,其结果应该是:

timerOne invoked ,the time:1001  
timerOne invoked ,the time:3001  

但是事与愿违,timerOne由于sleep(4000),休眠了4S,同时Timer内部是一个线程,导致timeOne所需的时间超过了间隔时间,结果:

timerOne invoked ,the time:1000  
timerOne invoked ,the time:5000  
问题二

如果TimerTask抛出RuntimeException,Timer会终止所有任务的运行。如下:

public class TimerTest04 {  
    private Timer timer;  
      
    public TimerTest04(){  
        this.timer = new Timer();  
    }  
      
    public void timerOne(){  
        timer.schedule(new TimerTask() {  
            public void run() {  
                throw new RuntimeException();  
            }  
        }, 1000);  
    }  
      
    public void timerTwo(){  
        timer.schedule(new TimerTask() {  
              
            public void run() {  
                System.out.println("我会不会执行呢??");  
            }  
        }, 1000);  
    }  
      
    public static void main(String[] args) {  
        TimerTest04 test = new TimerTest04();  
        test.timerOne();  
        test.timerTwo();  
    }  
}  

运行结果:timerOne抛出异常,导致timerTwo任务终止。

Exception in thread "Timer-0" java.lang.RuntimeException  
    at com.chenssy.timer.TimerTest04$1.run(TimerTest04.java:25)  
    at java.util.TimerThread.mainLoop(Timer.java:555)  
    at java.util.TimerThread.run(Timer.java:505)  

用ScheduledExecutorService替代Timer

对于Timer的缺陷,我们可以考虑使用 ScheduledThreadPoolExecutor 来替代。Timer是基于绝对时间的,对系统时间比较敏感,而ScheduledThreadPoolExecutor 则是基于相对时间;Timer是内部是单一线程,而ScheduledThreadPoolExecutor内部是个线程池,所以可以支持多个任务并发执行。

解决问题一
public class ScheduledExecutorTest {  
    private  ScheduledExecutorService scheduExec;  
      
    public long start;  
      
    ScheduledExecutorTest(){  
        this.scheduExec =  Executors.newScheduledThreadPool(2);    
        this.start = System.currentTimeMillis();  
    }  
      
    public void timerOne(){  
        scheduExec.schedule(new Runnable() {  
            public void run() {  
                System.out.println("timerOne,the time:" + (System.currentTimeMillis() - start));  
                try {  
                    Thread.sleep(4000);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
        },1000,TimeUnit.MILLISECONDS);  
    }  
      
    public void timerTwo(){  
        scheduExec.schedule(new Runnable() {  
            public void run() {  
                System.out.println("timerTwo,the time:" + (System.currentTimeMillis() - start));  
            }  
        },2000,TimeUnit.MILLISECONDS);  
    }  
      
    public static void main(String[] args) {  
        ScheduledExecutorTest test = new ScheduledExecutorTest();  
        test.timerOne();  
        test.timerTwo();  
    }  
}  

运行结果:

timerOne,the time:1003  
timerTwo,the time:2005  
解决问题二
public class ScheduledThreadPoolDemo01 {

    public static void main(String[] args) throws InterruptedException {

        final TimerTask task1 = new TimerTask() {

            @Override
            public void run() {
                throw new RuntimeException();
            }
        };

        final TimerTask task2 = new TimerTask() {

            @Override
            public void run() {
                System.out.println("task2 invoked!");
            }
        };

        ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
        pool.schedule(task1, 100, TimeUnit.MILLISECONDS);
        pool.scheduleAtFixedRate(task2, 0, 1000, TimeUnit.MILLISECONDS);

    }
}

代码基本一致,但是ScheduledExecutorService可以保证,task1出现异常时,不影响task2的运行:

task2 invoked!  
task2 invoked!  
task2 invoked!  
task2 invoked!  
task2 invoked! 

综上所述,基本说明了在以后的开发中尽可能使用ScheduledExecutorService(JDK1.5以后)替代Timer。

相关文章

  • 31-Timer的缺陷分析

    Timer的缺陷分析 Timer计时器可以定时(指定时间执行任务)、延迟(延迟5秒执行任务)、周期性地执行任务(每...

  • 缺陷分析漏斗模型

    做缺陷分析需要投入不少的人力和时间,所以在缺陷分析之前首先我们必须明确我们为什么要做缺陷分析,缺陷分析能给我们带来...

  • 缺陷分析入门

    缺陷分析也是测试工程师需要掌握的一个能力,但是很多时候大家只记得要提交缺陷、统计缺陷情况,而忽视了缺陷分析。其实每...

  • 软件缺陷分类——BUG类型

    缺陷报告 缺陷数据分析的数据指标 每天/周报告的新缺陷数目; 每天/周修复的缺陷数; 累计报告的缺陷数目; 累计修...

  • MantisBT管理缺陷

    软件测试就是为了发现软件中的缺陷,软件测试实质上是围绕着缺陷展开,报告缺陷、跟踪缺陷、分析缺陷等,所以缺陷的管理自...

  • 测试报告

    1.BUG分析 2.交互的缺陷 交互的缺陷引起的bug 为什么交互会有这么大的缺陷?交互稿的缺失 3.分析用户使用...

  • 缺陷分析笔记

    缺陷分析的基础是数据质量,该如何保证数据质量? 高质量的数据,是缺陷分析的基础,可以从两个方面大的方面来保证数据质...

  • HashMap,ArrayMap,SparyArray原理解析

    HashMap原理分析深度解读ArrayMap优势与缺陷

  • 高质量的缺陷分析:让自己少写 bug

    简介:缺陷分析做得好,bug 写得少。阿里资深技术专家和你分享如何进行高质量的缺陷分析,总结了 5 个要点,通过缺...

  • 软件缺陷智能分析技术(3) - 提升SZZ的准确率

    软件缺陷智能分析技术(3) - 提升SZZ的准确率 上一节我们介绍了在即时缺陷分析领域里有开创意义的SZZ算法。尽...

网友评论

    本文标题:31-Timer的缺陷分析

    本文链接:https://www.haomeiwen.com/subject/sfmvzxtx.html