这是我参与8月更文挑战的第20天,活动详情查看:8月更文挑战
背景
基于上一篇文章,本来做好的NTP对时功能,结果发现一个致命缺陷,将系统时间修改到未来某个时间,然后启动定时器,此时如果再将系统时间改回当前的正确时间,发现定时器挂起了,不运转了。遂查找资料,发现是定时器内部实现的原因。
原因分析
这个问题我们可以简单跟踪下Timer的源码,Timer中有两个重要的对象,一个是TaskQueue
,一个是TimerThread
。 TaskQueue 是一个队列,里面放的就是我们调用Timer.schedule时传的参数task。TimerThread 是一个线程,继承了Thread ,在它的run方法中调用了一个函数叫:mainLoop()
:
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的代码:
/**
* The main timer loop. (See class comment.)
*/
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) {
}
}
}
代码中出现了while(true),说明一直在重复地执行,我们的任务被放在了一个queue的队列中,mainLoop方法就是不断地执行队列里面所有的任务。
看下49、50、51三行代码:
//获取当前系统时间
currentTime = System.currentTimeMillis();
//下一次执行任务的时间
executionTime = task.nextExecutionTime;
taskFired = (executionTime<=currentTime)
如果下一次执行任务的时间小于等于当前时间,证明执行任务的时间已经到了,向下执行任务,如果不是,即taskFired=false,那么就等待,等待的时间为executionTime - currentTime
。
所以如果我们把系统时间调整为未来的时间,那么executionTime<=currentTime
肯定为true了,执行下次任务,程序运行正常。
但是如果我们再次调整系统时间为之前的时间,那么此时executionTime<=currentTime
肯定为false了,那么就会执行代码queue.wait(executionTime - currentTime)
,此时线程就会挂起了,至于等待的时间明显为executionTime - currentTime
。
解决方案
还好Android提供了ScheduledFuture
。
简单用法如下:
import android.util.Log;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
public class TimerUtil {
private ScheduledExecutorService service;
private ScheduledFuture<?> future;
public void startTimer(Runnable runable, int delay,int period) {
if (service == null) {
service = Executors.newScheduledThreadPool(1);//线程空间
future = service.scheduleAtFixedRate(runable, delay, period, TimeUnit.MILLISECONDS);
}
}
public void cancelTimer() {
Log.d("TimerUtil", "cancelTimer");
if (future != null) {
future.cancel(true);
future = null;
}
service = null;
}
}
上面的代码可以作为一个工具类替代Timer。