使用Timer執行定時任務很簡單,一般這樣子寫:
Timer timer = new Timer();
TimerTask task = new TimerTask() {
@Override
public void run() {
System.out.println("hello world");
}
};
timer.schedule(task, , );
以上代碼建立了一個定時器和定時任務,大部分情況下它都能正常工作,延遲10ms後,每隔2s就列印一個hello world。
但其實Timer是有一些問題的,程式中如果不注意就可能出現問題,下面來自《Java并發程式設計實戰》:
1.Timer在執行所有定時任務時隻會建立一個線程。如果某個任務的執行時間長度大于其周期時間長度,那麼就會導緻這一次的任務還在執行,而下一個周期的任務已經需要開始執行了,當然在一個線程内這兩個任務隻能順序執行,有兩種情況:對于之前需要執行但還沒有執行的任務,一是目前任務執行完馬上執行那些任務(按順序來),二是幹脆把那些任務丢掉,不去執行它們。至于具體采取哪種做法,需要看是調用schedule還是scheduleAtFixedRate。
2.如果TimerTask抛出了一個未檢出的異常,那麼Timer線程就會被終止掉,之前已經被排程但尚未執行的TimerTask就不會再執行了,新的任務也不能被排程了。
下面通過分析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 == ) { // Non-repeating, remove
queue.removeMin();
task.state = TimerTask.EXECUTED;
} else { // Repeating task, reschedule
queue.rescheduleMin(
task.period< ? 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 < )
throw new IllegalArgumentException("Negative delay.");
if (period <= )
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 < )
throw new IllegalArgumentException("Negative delay.");
if (period <= )
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實作心跳包的定時發送。
從JDK5開始引入了ThreadPoolExecutor,它可以用來實作定時任務,是以一般建議使用它來實作定時任務:
ScheduledExecutorService ses = Executors.newScheduledThreadPool();
ses.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("hello world");
}
}, , , TimeUnit.MILLISECONDS);
本部落格已停止更新,轉移到微信公衆号上寫文章,歡迎關注:Android進階驿站