为什么你不该用Timer
概述
在Java開發中,用過定時功能的同學一定不會對Timer感到陌生。不過,除了Timer,在Java 5之后又引入了一個定時工具ScheduledThreadPoolExecutor,那么我們應該如何在這兩個定時工具之間進行選擇呢?
一般情況下我們都建議使用ScheduledThreadPoolExecutor而不是Timer,主要原因有以下3點:
下面我們就來通過了解Timer與ScheduledThreadPoolExecutor的運行原理來理解上面幾個問題出現的原因。
Timer的運行機制
- TimerTask:任務類。內部持有nextExecutionTime變量,表示任務實際執行時間點,單位為毫秒,使用System.currentTimeMillis() + delay計算得出。
- TimerQueue:使用小根堆實現的優先隊列。按照TimerTask的實際執行時間點由小到大排序。
- TimerThread:顧名思義,這是實際執行任務的線程。
TimerThread會在Timer初始化后啟動,之后會進入mainLoop()方法,該方法會不斷從TimerQueue中取出時間點最小的TimerTask。如果該TimerTask的執行時間點已到,則直接調用TimerTask.run()執行;否則,調用wait()方法,等待相應的時間。
而我們調用Timer.schedule()方法,實際上是通過TimerQueue.add()方法,將TimerTask加入任務等待隊列。
這里還有一個需要注意的地方是:當加入任務的執行時間點是優先隊列中最小的時,就調用notify()方法喚醒TimerThread,而TimerThread在被喚醒后會重新調用TimerQueue.getMin()方法,再次調用wait(),不過這次的等待時間就變成了新加入任務的時間點。
ScheduledThreadPoolExecutor的運行機制
ScheduledThreadPoolExecutor繼承自ThreadPoolExecutor,對線程池的原理不了解的同學,可以看一下我的這篇文章:從零實現ImageLoader(三)—— 線程池詳解。
ScheduledThreadPoolExecutor的實現比Timer要復雜一些,不過要是理解了線程池的運行原理,其實也不難。它只不過是在ThreadPoolExecutor的基礎上使用自定義的阻塞隊列DelayedWorkQueue來實現任務定時功能。所以ScheduledThreadPoolExecutor的運行流程其實和ThreadPoolExecutor是差不多的。
- ScheduledFutureTask:任務類。內部持有time變量,單位為納秒,通過System.nanoTime() + delay計算得出。
- DelayedWorkQueue:使用小根堆實現的優先阻塞隊列,將ScheduledFutureTask按照從小到大的順序排列,同時在take()方法內實現阻塞操作。
- WorkerThread:這里為了簡單起見,我將線程池的核心線程和臨時線程統一寫成WorkerThread,但需要注意的是ScheduledThreadPoolExecutor是線程池的一個子類,所以線程池的那一套東西在ScheduledThreadPoolExecutor里也是有的。
光從這兩個圖上看,好像ScheduledThreadPoolExecutor和Timer的實現都大同小異,不過是換了一些名字,但實際上這兩個的實現還是有很大的不同的,不止因為ScheduledThreadPoolExecutor使用的是多線程。
在Timer里定時功能的實現主要依靠TimerThread.mainLoop()的等待,而ScheduledThreadPoolExecutor使用的是多線程,在每個線程里都單獨實現定時功能是不現實的,因此,ScheduledThreadPoolExecutor將定時功能放在了DelayedWorkQueue類里,而由于DelayedWorkQueue是阻塞隊列,所以定時任務的實現實際上就在DelayedWorkQueue.take()方法中。下面我們就來分析一下DelayedWorkQueue.take()到底做了什么。
Leader/Follower模式
在多線程網絡編程中,我們一般使用一個線程監聽端口,在接收到事件后再使用其他的線程去完成操作。這種情況下,在兩個線程之間的上下文切換開銷其實是很大的,于是我們有了Leader/Follower模式:
在Leader/Follower模式中,不存在一個專門用來監聽的線程,所有的線程都是等價的,而這些線程會不斷在Leader、Follower和Processor這三個狀態之間來回切換。
在程序中會保證每個時刻有且只有一個Leader,這個Leader就暫時充當了之前用來監聽端口線程的作用。而當有一個新的事件發生時,Leader不再是重新找一個線程去處理連接,而是自己轉化為Processor處理事件,并且重新指定一個Follower作為新的Leader。當事件處理完畢后,Processor又會轉化為Follower等待重新成為Leader。
take()方法的原理
這里的take()方法就借助了Leader/Follower模式的思想,同一時刻只有一個Leader線程,不過這里由于任務執行的時間點是已經確定了的,所以不再是等待一個觸發事件,而是等待最小任務所對應的延遲時間。其他的Follower線程則處于無限等待的狀態,直到當前Leader到達指定時間后轉化為Processor去處理任務,這時就會喚醒一個Follower作為下一任的Leader。而Processor在處理完任務后又會重新加入Follower進行等待。
絕對時間與相對時間
了解了Timer與ScheduledThreadPoolExecutor的運行機制,下面我們就來看一下Timer的這些缺陷究竟是怎么回事。
首先是絕對時間與相對時間的問題,可能有人已經發現,不管是TimerTask還是ScheduledFutureTask都是存儲的實際執行時間點,只不過一個是毫秒,一個是納秒,難道時間單位還會對這些有影響?確實,時間單位是不會對任務的執行有影響的,不過這里的玄機就在于這個時間的計算方式:System.currentTimeMillis()與System.nanoTime()。
System.currentTimeMillis()大家已經很清楚了,就是當前時間與1970年1月1日午夜的時間差的毫秒數,而System.nanoTime()又是什么呢?官方文檔里是這么說的:
此方法只能用于測量已過的時間,與系統或鐘表時間的其他任何時間概念無關。返回值表示從某一固定但任意的時間算起的毫微秒數。
這就是Timer與ScheduledThreadPoolExecutor一個是基于絕對時間而另一個是基于相對時間的原因。下面我們寫個例子來測試一下:
public static void main(String[] args) {System.out.println("Start:\t" + new Date());Executors.newSingleThreadScheduledExecutor().schedule(() -> {System.out.println("Executor:\t" + new Date());}, 60, TimeUnit.SECONDS);new Timer().schedule(new TimerTask() {public void run() {System.out.println("Timer:\t" + new Date());}}, 60000); }復制代碼輸出:
Start: Sun Oct 08 10:51:44 CST 2017 Executor: Sun Oct 08 10:51:41 CST 2017 Timer: Sun Oct 08 10:52:45 CST 2017復制代碼這里,我在啟動之后將系統的時鐘向后調了一分鐘,所以實際的啟動時間應該是10:50:44,由于ScheduledThreadPoolExecutor的等待時間與系統無關,所以在一分鐘后執行;而Timer是基于絕對時間的所以在10:52:45執行,實際上這時已經過去兩分鐘了。
單線程與多線程
Timer的第二個缺陷是,由于它使用的是單線程,所以長時間執行的任務會對其他任務產生影響。
public static void main(String[] args) {System.out.println("Start:\t\t\t" + new Date());ScheduledExecutorService service = Executors.newScheduledThreadPool(3);service.schedule(() -> {System.out.println("Executor 任務1:\t" + new Date());try {Thread.sleep(30000);} catch (InterruptedException e) {e.printStackTrace();}}, 60, TimeUnit.SECONDS);service.schedule(() -> {System.out.println("Executor 任務2:\t" + new Date());try {Thread.sleep(30000);} catch (InterruptedException e) {e.printStackTrace();}}, 60, TimeUnit.SECONDS);Timer timer = new Timer();timer.schedule(new TimerTask() {public void run() {System.out.println("Timer 任務1:\t\t" + new Date());try {Thread.sleep(30000);} catch (InterruptedException e) {e.printStackTrace();}}}, 60000);timer.schedule(new TimerTask() {public void run() {System.out.println("Timer 任務2:\t\t" + new Date());try {Thread.sleep(30000);} catch (InterruptedException e) {e.printStackTrace();}}}, 60000); }復制代碼輸出:
Start: Sun Oct 08 11:10:34 CST 2017 Executor 任務1: Sun Oct 08 11:11:34 CST 2017 Executor 任務2: Sun Oct 08 11:11:34 CST 2017 Timer 任務1: Sun Oct 08 11:11:34 CST 2017 Timer 任務2: Sun Oct 08 11:12:04 CST 2017復制代碼可以看到ScheduledThreadPoolExecutor中的兩個任務在等待一分鐘之后同時執行;而在Timer中的任務2卻因任務1長達半分鐘的執行時間,總共等了一分半鐘才得以執行。
異常處理
最后我們來看一下Timer與ScheduledThreadPoolExecutor對異常的處理情況:
Timer
Timer內部沒有對異常做任何處理,如果任務執行發生運行時異常,整個TimerThread都會崩潰:
public static void main(String[] args) {System.out.println("Start:\t\t\t" + new Date());Timer timer = new Timer();timer.schedule(new TimerTask() {public void run() {throw new RuntimeException("Timer 任務1");}}, 60000);timer.schedule(new TimerTask() {public void run() {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Timer 任務2:\t\t" + new Date());}}, 60000); }復制代碼輸出:
Start: Sun Oct 08 11:53:05 CST 2017 Exception in thread "Timer-0" java.lang.RuntimeException: Timer 任務1at main.Main$1.run(Main.java:32)at java.util.TimerThread.mainLoop(Timer.java:555)at java.util.TimerThread.run(Timer.java:505)復制代碼可以看到,任務1拋出的運行時異常導致整個Timer線程崩潰,任務2自然也沒有執行。
ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor中對異常的處理實際上是ThreadPoolExecutor類完成的,ThreadPoolExecutor在任務運行時對異常做了捕獲,并且將異常傳入了afterExecute()方法:
public class ThreadPoolExecutor extends AbstractExecutorService {final void runWorker(Worker w) {...Throwable thrown = null;try {task.run();} catch (RuntimeException x) {thrown = x; throw x;} catch (Error x) {thrown = x; throw x;} catch (Throwable x) {thrown = x; throw new Error(x);} finally {afterExecute(task, thrown);}...} }復制代碼我們來驗證一下:
public static void main(String[] args) {System.out.println("Start:\t\t\t" + new Date());ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();service.schedule(() -> {throw new RuntimeException("Executor 任務1");}, 60, TimeUnit.SECONDS);service.schedule(() -> {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Executor 任務2:\t" + new Date());}, 60, TimeUnit.SECONDS); }復制代碼輸出:
Start: Sun Oct 08 11:33:35 CST 2017 Executor 任務2: Sun Oct 08 11:34:36 CST 2017復制代碼可以看到這里雖然任務1拋出了運行時異常,但由于線程池內部完善的異常處理機制,任務2得以成功執行。
后記
看了這么多Timer的缺陷,你還在猶豫嗎?趕快放棄Timer,投入ScheduledThreadPoolExecutor的懷抱吧!
總結
以上是生活随笔為你收集整理的为什么你不该用Timer的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [分享] 数学学术资源站点
- 下一篇: 我的Java开发之路