美文网首页Android
Android Timer(定时器)踩坑记

Android Timer(定时器)踩坑记

作者: Magic旭 | 来源:发表于2022-07-27 10:58 被阅读0次

    背景

    由于网络需求需要通过发心跳来维持连接的建立,所以客户端需要通过计时器,每间隔一定事件发一次心跳请求到服务器,以此达到连接保活。我用了Timer来进行定时任务后,服务端童鞋找我说为啥同一秒会有重复的心跳请求发到服务器上呢?这就延伸出我们今天文章所要讲的内容了。

    问题

    业务场景是每隔10秒上报一次ping心跳,当09:50:33时候Timer执行了一次ping的上报任务后,下一次的上报的时间却是在09:50:54进行ping上报了(此次ping上报出现重复上报问题),中间间隔20几秒,在排查并非代码逻辑问题,把目光投向了定时器自身问题。

    日志心跳某一秒内重复无用心跳

    分析问题

    结合自身日志和Timer的源码阅读,可以知道此问题是由于使用Timer进行定时任务上报,当你的app的cpu资源竞争非常激烈时候,你的Timer里面的Thread没有办法准时获取cpu资源来执行开发者需要做的定时任务,当获取到cpu资源时,Timer就会为了弥补之前漏执行的定时任务,会在同一时刻进行1-n次的定时任务。

    前置知识

    刚入门面试的我们,多多少少都会被面试官问到sleep和wait的区别,当初的我们涉世尚浅,并不是太多关注这两个的区别,以为并没有什么用处,但看完我这篇文章你就明白当初面试官为什么问你这个问题了。这里先大概讲下,wait是让当前线程让出系统资源,释放锁,处于线程队列中进行等待;sleep是不让出系统资源,当前线程挂起一定时间,不释放锁。Timer里面源码的实现就是用了wait实现。

    源码解析
    1. 首先是从Timer的schedule函数开始看起来,大家对于这三个参数应该都有一定的认识,我这里就不展开细讲了。主要看的是scheduleAtFixedRate函数里的sched调用。注意sched第二个参数是当前系统时间+开发者所需的delay时间。
    
    Timer().scheduleAtFixedRate(object : TimerTask() {
                override fun run() {
                    ……
                }
            }, delayMills, periodMills)
    
    
    public void scheduleAtFixedRate(TimerTask task, long delay, long period) {
            ……
            sched(task, System.currentTimeMillis()+delay, period);
     }
    
    1. sched方法主要是把Timer的启动时间和间隔存储到Task对象里,再把Task对象加到队列里,看完了Timer的构造,我们下面看下Timer是如何运行。
    private void sched(TimerTask task, long time, long period) {
           ……
            synchronized(queue) {
              ……
                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();
            }
        }
    
    1. Timer内部有个TimerThread线程,Run内部实现为一个死循环,通过wait/wait(time)/notify 实现挂起/唤醒操作。在mainLoop里面有个逻辑缺陷就是,每次当前线程获取cpu资源时候,就会判断队列头部的Task是否到时间执行。如果未到时间,则wait剩余时间;如果到时间执行,则更新Task的下一次执行的时间(nextExecutionTime)。

    注意:那么问题就出现了,假如你的定时器任务执行完后,wait了下一次间隔时间,但是那个时间段cpu资源竞争很激烈,TimerThread根本抢不到cpu资源去执行,当到达下下一次间隔时间获取到cpu的资源时候,你的死循环就因为currentTime - executionTime >= 2倍的间隔时间,所以会同一时刻执行两个Runnable的回调,自然你Runnable回调也会在同一时刻做出重复的行为。

    class TimerThread extends Thread {
      public void run() {
            ……
            mainLoop();
            ……
        }
    }
    
    private void mainLoop() {
            while (true) {
                try {
                    TimerTask task;
                    boolean taskFired;
                    synchronized(queue) {
                        // 当Task队列为空时候,挂起系统资源,等待notify的唤醒
                        while (queue.isEmpty() && newTasksMayBeScheduled)
                            queue.wait();
                        ……
                        // 从队列中取出头部Task
                        task = queue.getMin();
                        synchronized(task.lock) {
                           ……
                            currentTime = System.currentTimeMillis();
                            //Task的执行sched函数时的系统时间
                            executionTime = task.nextExecutionTime;
                            //taskFired:true 执行时间到了,false 执行时间未到
                            if (taskFired = (executionTime<=currentTime)) {
                                if (task.period == 0) { // Non-repeating, remove
                                    queue.removeMin();
                                    task.state = TimerTask.EXECUTED;
                                } else { // Repeating task, reschedule
                                    //更新头部Task的nextExecutionTime时间
                                    queue.rescheduleMin(
                                      task.period<0 ? currentTime   - task.period
                                                    : executionTime + task.period);
                                }
                            }
                        }
                        if (!taskFired) // 任务还没有到时执行,挂起剩余的时间
                            queue.wait(executionTime - currentTime);
                    }
                    if (taskFired)  // 任务到时执行,回调Runnable
                        task.run();
                } catch(InterruptedException e) {
                }
            }
        }
    

    总结

    1. Timer的设计者也考虑到多报的情况,所以设计了如果你传进来的period为负数,就用当前系统时间+你的period间隔时间,从而选择漏报而不是多报一次,但是好像还有bug,所以外面的schedulexxx只要period为负数就会抛异常。

    2. 所有跑线程的任务都会有资源竞争的问题,如果想要解决此类问题,应该规划线程优先级,业务的优先级最多到哪个等级,上报、crash等线程优先级比业务等级高。只有明确线程等级,才能保证你的线程能按时获取cpu资源执行任务。

    3. 一起努力搬砖😄

    相关文章

      网友评论

        本文标题:Android Timer(定时器)踩坑记

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