注:java源碼分析部分如無特殊說明均基於 java8 版本。
注:本文基於ScheduledThreadPoolExecutor定時線程池類。
簡介
前面我們一起學習了普通任務、未來任務的執行流程,今天我們再來學習一種新的任務——定時任務。
定時任務是我們經常會用到的一種任務,它表示在未來某個時刻執行,或者未來按照某種規則重複執行的任務。
問題
(1)如何保證任務是在未來某個時刻才被執行?
(2)如何保證任務按照某種規則重複執行?
來個栗子
創建一個定時線程池,用它來跑四種不同的定時任務。
定時任務總體分爲四種:
(1)未來執行一次的任務,無返回值;
(2)未來執行一次的任務,有返回值;
(3)未來按固定頻率重複執行的任務;
(4)未來按固定延時重複執行的任務;
本文主要以第三種爲例進行源碼解析。
scheduleAtFixedRate()方法
提交一個按固定頻率執行的任務。
可以看到,這裏的處理跟未來任務類似,都是裝飾成另一個任務,再拿去執行,不同的是這裏交給了delayedExecute()方法去執行,這個方法是幹嘛的呢?
delayedExecute()方法
延時執行。
到這裏就結束了?!
實際上,這裏只是控制任務能不能被執行,真正執行任務的地方在任務的run()方法中。
還記得上面的任務被裝飾成了ScheduledFutureTask類的實例嗎?所以,我們只要看ScheduledFutureTask的run()方法就可以了。
ScheduledFutureTask類的run()方法
定時任務執行的地方。
可以看到,對於重複性任務,先調用FutureTask的runAndReset()方法,再設置下次執行的時間,最後再調用reExecutePeriodic()方法。
FutureTask的runAndReset()方法與run()方法類似,只是其任務運行完畢後不會把狀態修改爲NORMAL,有興趣的同學點進源碼看看。
再來看看reExecutePeriodic()方法。
到這裏是不是豁然開朗了,原來定時線程池執行重複任務是在任務執行完畢後,又把任務扔回了任務隊列中。
重複性的問題解決了,那麼,它是怎麼控制任務在某個時刻執行的呢?
OK,這就輪到我們的延時隊列登場了。
DelayedWorkQueue內部類
我們知道,線程池執行任務時需要從任務隊列中拿任務,而普通的任務隊列,如果裏面有任務就直接拿出來了,但是延時隊列不一樣,它裏面的任務,如果沒有到時間也是拿不出來的,這也是前面分析中一上來就把任務扔進隊列且創建Worker沒有傳入firstTask的原因。
我們這裏只拿一個take()方法出來分析。
大致的原理是,利用堆的特性獲取最快到時間的任務,即堆頂的任務:
(1)如果堆頂的任務到時間了,就讓它從隊列中了隊;
(2)如果堆頂的任務還沒到時間,就看它還有多久到時間,利用條件鎖等待這段時間,待時間到了後重新走(1)的判斷;
這樣就解決了可以在指定時間後執行任務。
其它
其實,ScheduledThreadPoolExecutor也是可以使用execute()或者submit()提交任務的,只不過它們會被當成0延時的任務來執行一次。
總結
實現定時任務有兩個問題要解決,分別是指定未來某個時刻執行任務、重複執行。
(1)指定某個時刻執行任務,是通過延時隊列的特性來解決的;
(2)重複執行,是通過在任務執行後再次把任務加入到隊列中來解決的。
彩蛋
到這裏基本上普通的線程池的源碼解析就結束了,這種線程池是比較經典的實現方式,整體上來說,效率相對不是特別高,因爲所有的工作線程共用同一個隊列,每次從隊列中取任務都要加鎖解鎖操作。
那麼,能不能給每個工作線程配備一個任務隊列呢,在提交任務的時候就把任務分配給指定的工作線程,這樣在取任務的時候就不需要頻繁的加鎖解鎖了。