天天看點

Java線程池架構2-多線程排程器(ScheduledThreadPoolExecutor)

在前面介紹了java的多線程的基本原理資訊:《java線程池架構原理和源碼解析(threadpoolexecutor)》,本文對這個java本身的線程池的排程器做一個簡單擴充,如果還沒讀過上一篇文章,建議讀一下,因為這是排程器的核心元件部分。

我們如果要用java預設的線程池來做排程器,一種選擇就是timer和timertask的結合,在以前的文章:《timer與timertask的真正原理&使用介紹》中有明确的說明:一個timer為一個單獨的線程,雖然一個timer可以排程多個timertask,但是對于一個timer來講是串行的,至于細節請參看對應的那篇文章的内容,本文介紹的多線程排程器,也就是定時任務,基于多線程排程完成,當然你可以為了完成多線程使用多個timer,隻是這些timer的管理需要你來完成,不是一個架構體系,而schedulethreadpoolexecutor提供了這個功能,是以我們第一要搞清楚是如何使用排程器的,其次是需要知道它的内部原理是什麼,也就是知其然,再知其是以然!

首先如果我們要建立一個基于java本身的排程池通常的方法是:

executors.newscheduledthreadpool(int);

當有重載方法,我們最常用的是這個就從這個,看下定義:

其實内部是new了一個執行個體化對象出來,并傳入大小,此時就跟蹤到scheduledthreadpoolexecutor的構造方法中:

public scheduledthreadpoolexecutor(int corepoolsize) {

        super(corepoolsize, integer.max_value, 0,timeunit.nanoseconds,

              new delayedworkqueue());

}

你會發現調用了super,而super你跟蹤進去會發現,是threadpoolexecutor中,那麼scheduledthreadpoolexecutor和threadpoolexecutor有何差別,就是本文要說得重點了,首先我們留下個引子,你發現在定義隊列的時候,不再是上文中提到的linkedblockingqueue,而是delayedworkqueue,那麼細節上我們接下來就是要講解的重點,既然他們又繼承關系,其實搞懂了不同點,就搞懂了共同點,而且有這樣的關系大多數應當是共同點,不同點的猜測:這個是要實作任務排程,任務排程不是立即的,需要延遲和定期做等情況,那麼是如何實作的呢?

這就是我們需要思考的了,通過源碼考察,我們發現,他們都有execute方法,隻是scheduledthreadpoolexecutor将源碼進行了重寫,并且還有以下四個排程器的方法:

那麼這四個方法有什麼差別呢?其實第一個和第二個差別不大,一個是runnable、一個是callable,内部包裝後是一樣的效果;是以把頭兩個方法幾乎當成一種排程,那麼三種情況分别是:

1、 進行一次延遲排程:延遲delay這麼長時間,機關為:timeunit傳入的的一個基本機關,例如:timeunit.seconds屬于提供好的枚舉資訊;(适合于方法1和方法2)。

2、 多次排程,每次依照上一次預計排程時間進行排程,例如:延遲2s開始,5s一次,那麼就是2、7、12、17,如果中間由于某種原因導緻線程不夠用,沒有得到排程機會,那麼接下來計算的時間會優先計算進去,因為他的排序會被排在前面,有點類似timer中的:scheduleatfixedrate方法,隻是這裡是多線程的,它的方法名也叫:scheduleatfixedrate,是以這個是比較好記憶的(适合方法3)

3、 多次排程,每次按照上一次實際執行的時間進行計算下一次時間,同上,如果在第7秒沒有被得到排程,而是第9s才得到排程,那麼計算下一次排程時間就不是12秒,而是9+5=14s,如果再次延遲,就會延遲一個周期以上,也就會出現少調用的情況(适合于方法3);

4、 最後補充execute方法是一次排程,期望被立即排程,時間為空:

public void execute(runnable command) {

       if (command == null)

           throw new nullpointerexception();

       schedule(command, 0, timeunit.nanoseconds);

    }

我們簡單看看scheduleatfixedrate、schedulewithfixeddelay對下面的分析會更加有用途:

你是否發現,兩段源碼唯一的差別就是在unit.tonanos(int)這唯一一個地方,scheduleatfixedrate裡面是直接傳入值,而schedulewithfixeddelay裡面是取了相反數,也就是假如我們都傳入正數,schedulewithfixeddelay其實就取反了,沒有任何差別,你是否聯想到前面文章介紹timer中類似的處理手段通過正負數區分時間間隔方法,為0代表僅僅排程一次,其實在這裡同樣是這樣的,他們也同樣有一個問題就是,如果你傳遞負數,方法的功能正好是相反的。

而你會發現,不論是那個schedule方法裡頭,都會建立一個scheduledfuturetask類的執行個體,此類究竟是何方神聖呢,我們來看看。

scheduledfuturetask的類(schedulethreadpoolexecutor的私有的内部類)來進行排程,那麼可以看看内部做了什麼操作,如下:

最核心的幾個參數正好對應了排程的延遲的構造方法,這些參數如何用起來的?那麼它還提供了什麼方法呢?

這裡發現了,他們可以運作,且判定時間的方法是getdelay方法我們知道了。

對比時間的方法是:compareto,傳入了參數類型為:delayed類型,不難猜測出,scheduledfuturetask和delayed有某種繼承關系,沒錯,scheduledfuturetask實作了delayed的接口,隻是它是間接實作的;并且delayed接口繼承了comparable接口,這個接口可用來幹什麼?看過我前面寫的一篇文章關于中文和對象排序的應該知道,這個是用來自定義對比和排序的,我們的排程任務是一個對象,是以需要排序才行,接下來我們回溯到開始定義的代碼中,找一個實際調用的代碼來看看它是如何啟動到run方法的?如何排序的?如何調用延遲的?就是我們下文中會提到的,而這裡我們先提出問題,後文我們再來說明這些問題。

我們先來看下run方法的一些定義。

可以看到run方法首先通過isperiod()判定是否為時間片,判定的依據就是我們說的時間片是否“不為零”,如果不是周期任務,就直接運作一次,如果是周期任務,則除了運作還會計算下一次執行的時間,并将其再次放入等待隊列,這裡對應到scheduleatfixedrate、schedulewithfixeddelay這兩個方法一正一負,在這裡得到判定,并且将為負數的取反回來,負負得正,java就是這麼幹的,呵呵,是以不要認為什麼是不可能的,隻要好用什麼都是可以的,然後計算的時間一個是基于标準的time加上一個時間片,一個是根據目前時間計算一個時間片,在上文中我們已經明确說明了兩者的差別。

以:schedule方法為例:

其實這個方法内部建立的就是一個我們剛才提到的:scheduledfuturetask,外面又包裝了下叫做runnablescheduledfuture,也就是适配了下而已,呵呵,代碼裡面就是一個return操作,java這樣做的目的是友善子類去擴充。

關鍵是delayedexecute(t)方法中做了什麼?看名稱是延遲執行的意思,難道java的線程可以延遲執行,那所有的任務線程都在運作狀态?

它的源碼是這樣的:

我們主要關心prestartcorethread()和super.getqueue().add(command),因為如果系統關閉,這些讨論都沒有意義的,我們分别叫他們第二小段代碼和第三小段代碼。

第二個部分如果線程數小于核心線程數設定,那麼就調用一個prestartcorethread(),看方法名應該是:預先啟動一個核心線程的意思,先看完第三個部分,再跟蹤進去看源碼。

第三個部分很明了,就是調用super.getqueue().add(command);也就是說直接将任務放入一個隊列中,其實super是什麼?super就是我們上一篇文章所提到的threadpoolexecutor,那麼這個queue就是上一篇文章中提到的等待隊列,也就是任何schedule任務首先放入等待隊列,然後等待被排程的。

這個代碼是否似曾相似,沒錯,這個你在上一篇文章介紹threadpoolexecutor的時候就見到過,說明不論是threadpoolexecutor還是schedulethreadpoolexecutor他們的thread都是由一個worker來處理的(上一篇文章有介紹),而這個worker處理的基本機制就是将目前任務執行後,不斷從線程等待隊列中擷取資料,然後用以執行,直到隊列為空為止。

那麼他們的差別在哪裡呢?延遲是如何實作的呢?和我們上面介紹的scheduledfuturetask又有何關系呢?

那麼我們回過頭來看看schedulethreadpool的定義是如何的。

發現它和threadpoolexecutor有個定義上很大的差別就是,threadpoolexecutor用的是linkedblockingqueue(當然可以修改),它用的是delayedweorkqueue,而這個delayedworkqueue裡面你會發現它僅僅是對java.util.concurrent.delayedqueue類一個簡單通路包裝,這個隊列就是等待隊列,可以看到任務是被直接放到等待隊列中的,是以取資料必然從這裡擷取,而這個延遲的隊列有何神奇之處呢,它又是如何實作的呢,我們從什麼地方下手去看這個delayworkqueue?

我們還是回頭看看worker裡面的run方法(上一篇文章中已經講過):

這裡面要調用等待隊列就是gettask()方法:

你會發現,如果沒有設定逾時,預設隻會通過workqueue.take()方法擷取資料,那麼我們就看take方法,而增加到隊列裡面的方法自然看offer相關的方法。接下來我們來看下delayqueue這個隊列的take方法:

這裡的for就是要找到資料為止,否則就等着,而這個“q”和“available”是什麼呢?

private transient final condition available = lock.newcondition();

private final priorityqueue<e> q = new priorityqueue<e>();

怎麼裡面還有一層隊列,不用怕,從這裡你貌似看出點名稱意味了,就是它是優先級隊列,而對于任務排程來講,優先級的方式就是時間,我們用這中猜測來繼續深入源碼。

上面首先擷取這個隊列的第一個元素,若為空,就等待一個“available”發出的信号,我們可以猜測到這個offer的時候會發出的信号,一會來驗證即可;若不為空,則通過getdelay方法來擷取時間資訊,這個getdelay方法就用上了我們開始說的scheduledfuturetask了,如果是時間大于0,則也進入等待,因為還沒開始執行,等待也是“available”發出信号,但是有一個最長時間,為什麼還要等這個信号,是因為有可能進來一個新的任務,比這個等待的任務還要先執行,是以要等這個信号;而最多等這麼長時間,就是因為如果這段時間沒任務進來肯定就是它執行了。然後就傳回的這個值,被worker(上面有提到)拿到後調用其run()方法進行運作。

那麼寫入隊列在那裡?他們是如何排序的?

我們看看隊列的寫入方法是這樣的:

隊列也是首先取出第一個(後面會用來和目前任務做比較),而這裡“q”是上面提到的“priorityqueue”,看來offer的關鍵還在它的裡面,我們看看調用過程:

你是否發現,compareto也用上了,就是我們前面描述一大堆的:scheduledfuturetask類中的一個方法,那麼run方法也用上了,這個過程貌似完整了。

我們再來理一下思路:

1、調用的thread的包裝,由在threadpoolexecutor中的worker調用你傳入的runnable的run方法,變成了worker調用runnable的run方法,由它來處理時間片的資訊調用你傳入的線程。

2、scheduledfuturetask類在整個過程中提供了基礎參考的方法,其中最為關鍵的就是實作了接口comparable,實作内部的compareto方法,也實作了delayed接口中的getdelay方法用以判定時間(當然delayed接口本身也是繼承于comparable,我們不要糾結于細節概念就好)。

3、等待隊列由在threadpoolexecutor中預設使用的linkedblockingqueue換成了delayqueue(它是被delayworkqueue包裝了一下子,沒多大差別),而delayqueue主要提供了一個信号量“available”來作為寫入和讀取的信号控制開關,通過另一個優先級隊列“priorityqueue”來控制實際的隊列順序,他們的順序就是基于上面提到的scheduledfuturetask類中的compareto方法,而是否運作也是基于getdelay方法來實作的。

4、scheduledfuturetask類的run方法會判定是否為時間片資訊,如果為時間片,在執行完對應的方法後,開始計算下一次執行時間(注意判定時間片大于0,小于0,分别代表的是以目前執行完的時間為準計算下一次時間還是以目前時間為準),這個在前面有提到。

5、它是支援多線程的,和timer的機制最大的差別就在于多個線程會最征用這個隊列,隊裡的排序方式和timer有很多相似之處,并非完全有序,而是通過位移動來盡量找到合适的位置,有點類似貪心的算法,呵呵。