Android Timer(定时器)踩坑记

背景

由于网络需求需要通过发心跳来维持连接的建立,所以客户端需要通过计时器,每间隔一定事件发一次心跳请求到服务器,以此达到连接保活。我用了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. 一起努力搬砖😄

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容