天天看點

Java中的Timer源碼分析及缺陷

使用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進階驿站

Java中的Timer源碼分析及缺陷