配景
由于网络需求须要通过发心跳来维持毗连的创建,以是客户端须要通过计时器,每隔断肯定变乱发一次心跳哀求到服务器,以此到达毗连保活。我用了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实现。
源码分析
- 起首是从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); }
- 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(); } }
- 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) { } } }总结
- Timer的设计者也思量到多报的环境,以是设计了假如你传进来的period为负数,就用当前体系时间+你的period隔断时间,从而选择漏报而不是多报一次,但是好像尚有bug,以是外貌的schedulexxx只要period为负数就会抛非常。
- 全部跑线程的任务都会有资源竞争的题目,假如想要办理此类题目,应该规划线程优先级,业务的优先级最多到哪个品级,上报、crash等线程优先级比业务品级高。只有明白线程品级,才气包管你的线程能按时获取cpu资源实验任务。
- 一起积极搬砖?
|