作者:點先生 時間:2019.3.27
逼逼兩句
Q:定時、延時任務有幾種方式可以實現?
A:Handler、Timer、ScheduledThreadPool、AlarmManager
Handler機制大家應該都爛熟於心了,今天我來講講Timer這個不常被問到的定時器。 改日再說執行緒池,預計是週日。
Timer機制包含了四個主要核心類:Timer,TaskQueue,TimerThread,TimerTask。我們們一個個來了解。
Timer
Timer類載入時建立新的任務佇列,新的定時器執行緒。並將兩個繫結起來。
public class Timer {
private final TaskQueue queue = new TaskQueue();
private final TimerThread thread = new TimerThread(queue);
}
複製程式碼
初始化Timer
反正就是給thread設定名字,或者設定是否是守護執行緒,最後開啟執行緒;這個thread,就是TimerThread。
呼叫這四個方法可以執行定時任務、延時任務、週期執行任務。
這兩個方法與上面最後兩個方法很類似,不同的地方在於sched()的最後一個引數,傳入當前值或是相反數值,這裡的具體影響後面會介紹到。sched()的核心程式碼為:
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();
}
}
複製程式碼
主要就是將初始化後的task進行賦值,然後加入佇列。 至此Timer裡面就還有兩個方法沒說到。
public void cancel(){ 清空佇列,通知佇列 }
public int purge(){ 將佇列中狀態為“CANCELLED”的任務移除,並重排序佇列。 }
複製程式碼
TaskQueue
任務佇列實際上就是一個TimerTask的大小為128的陣列。size表示佇列中的任務數。 其他的就是一些操作此陣列的方法
int size() { 獲取當前任務數 }
void add(TimerTask task){ 新增任務到陣列,並 fixUp(size),第一個元素的位置為1,非0。 }
TimerTask getMin(){ 得到最近的一個任務 }
TimerTask get(int i){ 得到i元素 }
void removeMin(){ 移除最近的一個任務,並 fixDown(1) }
void quickRemove(int i){ 快速移速某個任務,不重排序 }
void rescheduleMin(long newTime){ 重新設定最近任務的執行時間,並 fixDown(1) }
boolean isEmpty(){ 判斷佇列是否為空 }
void clear(){ 清空佇列 }
void fixUp(int k){ 排序方法1 }
void fixDown(int k){ 排序方法2 }
void heapify(){ 排序方法3 }
複製程式碼
三種排序方式不再此深探究。在此留下一個疑問,為何第一個任務新增進來給的位置是1,非0;
TimerThread
class TimerThread extends Thread {
boolean newTasksMayBeScheduled = true;
private TaskQueue queue
TimerThread(TaskQueue queue) {
this.queue = queue;
}
public void run() {
try { mainLoop();
} finally {
synchronized(queue) {
newTasksMayBeScheduled = false;
queue.clear();
}
}
}
複製程式碼
- TimerThread是一個Thread。
- 初始化的時候繫結對應TaskQueen。
- TimerThread正常執行時,就一直迴圈取佇列訊息執行任務。當執行緒被殺死,或者其他異常出現時候,便會清空佇列。 tips:newTasksMayBeScheduled 是標誌這當前是否對定時器物件保持引用。當佇列中不再有任務,則為真。
最後來看看mainLoop()中的核心程式碼:
private void mainLoop() {
while (true) {
try {
synchronized(queue) {
//其他邏輯
long currentTime, executionTime;
task = queue.getMin();
synchronized(task.lock) {
//其他邏輯
currentTime = System.currentTimeMillis();
executionTime = task.nextExecutionTime;
if (taskFired = (executionTime<=currentTime)) {
if (task.period == 0) {
queue.removeMin();
task.state = TimerTask.EXECUTED;
} else {
queue.rescheduleMin(
task.period<0 ? currentTime - task.period
: executionTime + task.period);
}
}
}
if (!taskFired)
queue.wait(executionTime - currentTime);
}
if (taskFired)
task.run();
} catch(InterruptedException e) {
}
}
}
複製程式碼
執行緒執行起來之後,就一直在取最近的訊息對比當前時間,執行時間到了,就看是否是一次性任務。如果是一次性任務,就更改任務狀態。如果是週期任務,就把給任務設定新的執行時間再入佇列。如果一開始執行時間就沒到,就wait當前佇列。最後根據執行時間是否到達,執行取出來的最近任務。
tips:週期任務重置時間時,有兩種時間,當period<0時currentTime - task.period ,當period>0時executionTime + task.period。根據Timer中sched()和scheduleAtFixedRate()的區別能推斷出,前者程式碼表示,當前任務執行完之後,再進入period時間。後這程式碼表示,當前任務執行開始的時,就進入period時間。
TimerTask
TimerTask是個抽象類,實現了Runnable介面。內部擁有四個屬性,三個方法。
public abstract class TimerTask implements Runnable {
final Object lock = new Object(); //物件鎖,用於維護執行緒安全;
int state = VIRGIN;//狀態值
long nextExecutionTime;//下一次執行的時間
long period = 0;//週期時間
static final int VIRGIN = 0;
static final int SCHEDULED = 1;
static final int EXECUTED = 2;
static final int CANCELLED = 3;
}
複製程式碼
VIRGIN :初始化預設值,表達此任務還沒被加入執行佇列。
SCHEDULED :任務被安排準備執行,已加入執行佇列
EXECUTED : 任務正在執行或者已經執行,還沒被取消。
CANCELLED :任務已經被取消
public abstract void run();
public boolean cancel() {
synchronized(lock) {
boolean result = (state == SCHEDULED);
state = CANCELLED;
return result;
}
}
public long scheduledExecutionTime() {
synchronized(lock) {
return (period < 0 ? nextExecutionTime + period
: nextExecutionTime - period);
}
}
複製程式碼
繼承TimerTask或者匿名內部類建立都可以實現執行定時任務,將要執行的動作寫在run()裡面即可。 cancel()返回當前任務狀態值是否是SCHEDULED,再將其狀態值改成CANCELLED。 scheduledExecutionTime()返回的是下一次執行的時間。
Timer與Handler
- Timer機制的結構跟Handler類似,具體處理不一樣,但都分為四大結構。
- Timer、Handler:主控器
- TimerTask、Message:訊息/任務
- TimerQueue、MessageQueue:訊息/任務佇列
- TimerThread、Looper:迴圈取訊息/任務
優缺點 | Handler | Timer |
---|---|---|
執行同一個非週期任務 | 只需要再發一次訊息 | 需要建立新的TimerTask |
通訊 | 執行緒間通訊靈活 | TimerTask執行在子執行緒中 |
可靠性 | 週期執行任務比較可靠 | 週期執行任務不可靠(下面解釋) |
記憶體洩漏 | 容易洩漏 | 容易洩漏 |
記憶體消耗 | 小 | 相對較大 |
靈活性 | 依賴looper,不靈活 | Timer不依賴其他類 |
Timer執行的週期任務容易被自身干擾。(當耗時任務在sched()中執行時候,會大大延遲下一次任務的執行;當耗時任務需要操作同一個物件在scheduleAtFixedRate()中執行的時候,拿不到任務物件,等待上一次的任務釋放鎖。)
小結
Handler適合大多數場景,且好處理。 Timer只適合執行耗時比較少的重複任務。 難怪Timer相關文章熱度這麼低,看完原始碼才知道,是個小辣雞。這兩天時間算是浪費了。
最後希望大家多多關注我們的部落格團隊:天星技術部落格https://juejin.im/user/5afa539751882542aa42e5c5