1. 程式人生 > 其它 >一文搞懂Java執行緒池

一文搞懂Java執行緒池

主要包括建立執行緒的方式、ThreadPoolExecutor的七大引數詳解、執行緒池的執行流程、執行緒池執行緒數如何設計等要點。

一、建立執行緒的方式

1 繼承 Thread 類並重寫 run 方法。實現簡單,但不符合里氏替換原則,不可以繼承其他類。步驟:

(1)繼承Thread類並重寫run方法,該run方法的方法體就代表了執行緒要完成的任務。因此把run()方法稱為執行體。

(2)建立執行緒物件並呼叫start方法進行啟動

2 實現 Runnable 介面並重寫 run 方法。避免了單繼承侷限性,程式設計更加靈活,實現解耦。步驟:

(1)實現Runnable介面並重寫run方法

(2)建立執行緒物件並呼叫start方法進行啟動

3 實現 Callable 介面並重寫 call 方法。可以獲取執行緒執行結果的返回值,並且可以丟擲異常。步驟:

(1)定義一個類實現Callable介面,並實現call()方法,該call()方法將作為執行緒執行體,並且有返回值。

(2)建立執行緒物件,使用FutureTask類來包裝Callable物件,並呼叫start方法進行啟動 FutureTask ft = new FutureTask<>(mc);

(3)呼叫FutureTask物件的get()方法來獲得子執行緒執行結束後的返回值

4 使用 Executors 工具類建立執行緒池

二、為什麼要有執行緒池

想想我們之前沒用執行緒池的時候,每次建立執行緒都是:new Thread(() -> {...}),再調start()方法來執行執行緒。這就會帶來一系列問題,比如:執行緒的建立和銷燬都是很耗時很浪費效能的操作。再者,簡單的new兩三個Thread還好,但若需要上百個執行緒呢?而且用完再銷燬掉時又要一個一個的進行銷燬。那這上百個執行緒的建立和銷燬的效能是很糟糕的!

執行緒池誕生就是為了解決上述問題。其核心思想就是:執行緒複用。也就是說執行緒用完後不銷燬,放到池子裡等著新任務的到來,反覆利用N個執行緒來執行所有新老任務。這帶來的開銷只會是那N個執行緒的建立,而不是每來一個請求都帶來一個執行緒的從生到死的過程。

因此使用執行緒池的好處與優點:

  • 降低資源消耗。通過重複利用已建立的執行緒降低執行緒建立和銷燬造成的消耗。

  • 提高響應速度。當任務到達時,任務可以不需要的等到執行緒建立就能立即執行。

  • 提高執行緒的可管理性。使用執行緒池可以對執行緒進行統一的分配,調優和監控。

三、建立執行緒池的方法

3.1 七大引數

可以通過 Executors 的靜態工廠方法建立執行緒池:

不建議使用Executors來建立,而是使用ThreadPoolExcecutor的方式,這樣的處理⽅式讓寫的同學更加明確執行緒池的運⾏規則,規避資源耗盡的⻛險。

上原始碼(有七大引數):

public ThreadPoolExecutor(
    int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler) {}

corePoolSize:核心執行緒數

預設情況下,在建立了執行緒池後,執行緒池中的執行緒數為0,當有任務來之後,就會建立一個執行緒去執行任務。核⼼執行緒數定義了最⼩可以同時運⾏的執行緒數量。當執行緒池中的執行緒數目達到corePoolSize後,就會把到達的任務放到工作隊列當中。預設不會被回收掉,但是如果設定了allowCoreTimeOut為true,那麼當核心執行緒閒置時,也會被回收。

maximumPoolSize:最大執行緒數

當佇列中存放的任務達到佇列容量的時候,當前可以同時執行的執行緒數量變為最大執行緒數。如果與核心執行緒數設定相同代表固定大小執行緒池。

workQueue:工作佇列

當執行緒請求數大於等於 corePoolSize 時執行緒會進入工作佇列。阻塞佇列,用來儲存等待執行的任務,新任務被提交後,會先進入到此工作佇列中,任務排程時再從佇列中取出任務。這裡的阻塞佇列有以下幾種選擇:

  • ArrayBlockingQueue:基於陣列的有界阻塞佇列,按FIFO排序;

  • LinkedBlockingQueue:基於連結串列的無界阻塞佇列(其實最大容量為Interger.MAX),按照FIFO排序;

  • SynchronousQueue:一個不快取任務的阻塞佇列,也就是說新任務進來時,不會快取,而是直接被排程執行該任務;

  • PriorityBlockingQueue:具有優先順序的無界阻塞佇列,優先順序通過引數Comparator實現。

unit:單位,keepAliveTime 的時間單位。比如:TimeUnit.MILLISECONDSTimeUnit.SECONDS

keepAliveTime:執行緒空閒時間

執行緒空閒時間達到該值後會被銷燬,直到只剩下 corePoolSize 個執行緒為止,避免浪費記憶體資源。

threadFactory:執行緒工廠

當執行緒池需要新的執行緒時,會用threadFactory來生成新的執行緒

預設採用的是DefaultThreadFactory,主要負責建立執行緒。newThread()方法。創建出來的執行緒都在同一個執行緒組且優先順序也是一樣的。即用來生產一組相同任務的執行緒。可以給執行緒命名,有利於分析錯誤。

handler:拒絕策略

  • AbortPolicy 丟棄任務並丟擲異常;

功能:當觸發拒絕策略時,直接丟擲拒絕執行的異常,中止策略的意思也就是打斷當前執行流程

使用場景:這個就沒有特殊的場景了,但是一點要正確處理丟擲的異常。

ThreadPoolExecutor中預設的策略就是AbortPolicy,ExecutorService介面的系列ThreadPoolExecutor因為都沒有顯示的設定拒絕策略,所以預設的都是這個。但是請注意,ExecutorService中的執行緒池例項佇列都是無界的,也就是說把記憶體撐爆了都不會觸發拒絕策略。當自己自定義執行緒池例項時,使用這個策略一定要處理好觸發策略時拋的異常,因為他會打斷當前的執行流程。

  • CallerRunsPolicy 呼叫執行自己的執行緒執行任務。但是這種策略會降低對於新任務提交速度,影響程式的整體效能。另外,這個策略喜歡增加佇列容量。如果您的應用程式可以承受此延遲並且你不能丟棄任何一個任務請求的話,你可以選擇這個策略;

    功能:當觸發拒絕策略時,只要執行緒池沒有關閉,就由提交任務的當前執行緒處理。

    使用場景:一般在不允許失敗的、對效能要求不高、併發量較小的場景下使用,因為執行緒池一般情況下不會關閉,也就是提交的任務一定會被執行,但是由於是呼叫者執行緒自己執行的,當多次提交任務時,就會阻塞後續任務執行,效能和效率自然就慢了。

  • DiscardOldestPolicy 表示拋棄佇列裡等待最久的任務並把當前任務加入佇列;

    功能:直接靜悄悄的丟棄這個任務,不觸發任何動作

    使用場景:如果你提交的任務無關緊要,你就可以使用它 。因為它就是個空實現,會悄無聲息的吞噬你的的任務。所以這個策略基本上不用了

  • DiscardPolicy 表示直接拋棄當前任務但不丟擲異常。

    功能:如果執行緒池未關閉,就彈出佇列頭部的元素,然後嘗試執行

    使用場景:這個策略還是會丟棄任務,丟棄時也是毫無聲息,但是特點是丟棄的是老的未執行的任務,而且是待執行優先順序較高的任務。基於這個特性,我能想到的場景就是,釋出訊息,和修改訊息,當訊息釋出出去後,還未執行,此時更新的訊息又來了,這個時候未執行的訊息的版本比現在提交的訊息版本要低就可以被丟棄了。因為佇列中還有可能存在訊息版本更低的訊息會排隊執行,所以在真正處理訊息的時候一定要做好訊息的版本比較。

3.2 示例

3.3 內建封裝好的的幾個執行緒池

可以這樣來建立:ExecutorService MyExecutorService = Executors.newCachedThreadPool();

  • newFixedThreadPool,固定大小的執行緒池,核心執行緒數也是最大執行緒數,不存在空閒執行緒,keepAliveTime = 0。該執行緒池使用的工作佇列是無界阻塞佇列 LinkedBlockingQueue,適用於負載較重的伺服器。

  • newSingleThreadExecutor,使用單執行緒,相當於單執行緒序列執行所有任務,適用於需要保證順序執行任務的場景。與單執行緒效能比較:雖然同是一個執行緒在工作,但是使用單執行緒池效率高多了。

  • newCachedThreadPool,該方法返回一個可根據實際情況調整執行緒數量的執行緒池。執行緒池的執行緒數量不確定,但若有空閒執行緒可以複用,則會優先使用可複用的執行緒。若所有執行緒均在工作,又有新的任務提交,則會建立新的執行緒處理任務。所有執行緒在當前任務執行完畢後,將返回執行緒池進行復用。

  • newScheduledThreadPool:支援定期及週期性任務執行,適用需要多個後臺執行緒執行週期任務,同時需要限制執行緒數量的場景。與 newCachedThreadPool 的區別是不回收工作執行緒。原理:ScheduedThreadPoolExecutor是先把任務放到一個DelayQueue延遲佇列中,然後再啟動一個執行緒,再去佇列中取週期時間離當前時間最近的那個任務。ScheduedThreadPoolExecutor維護了一個DelayQueue儲存等待的任務,DelayQueue裡面有一個PriorityQueue優先順序佇列,他會根據time的時間大小排序,時間越小的越靠前。DelayQueue也是一個無界佇列,但是初始大小為16,超過16會進行一次擴容。有三種提交任務的方式:

    • schedule,特定時間延時後執行一次任務
    • scheduledAtFixedRate,固定週期執行任務(與任務執行時間無關,週期是固定的)
    • scheduledWithFixedDelay,固定延時執行任務(與任務執行時間有關,延時從上一次任務完成後開始)

四、執行緒池處理任務的流程

① 核心執行緒池未滿,建立一個新的執行緒執行任務,此時 workCount < corePoolSize,需要獲取全域性鎖。

② 如果核心執行緒池已滿,工作佇列未滿,將任務儲存在工作佇列,此時 workCount >= corePoolSize。

③ 如果工作佇列已滿,執行緒數小於最大執行緒數就建立一個新執行緒處理任務,此時 workCount < maximumPoolSize,這一步也需要獲取全域性鎖。

④ 如果超過最大執行緒數,按照拒絕策略來處理任務,此時 workCount > maximumPoolSize。

執行緒池建立執行緒時,會將執行緒封裝成工作執行緒 Worker,Worker 在執行完任務後還會迴圈獲取工作佇列中的任務來執行。

舉例說明

執行緒池引數配置:核心執行緒5個,最大執行緒數10個,佇列長度為100。

那麼執行緒池啟動的時候不會建立任何執行緒,假設請求進來6個,則會建立5個核心執行緒來處理五個請求,另一個沒被處理到的進入到佇列。這時候有進來99個請求,執行緒池發現核心執行緒滿了,佇列還在空著99個位置,所以會進入到佇列裡99個,加上剛才的1個正好100個。這時候再次進來5個請求,執行緒池會再次開闢五個非核心執行緒來處理這五個請求。目前的情況是執行緒池裡執行緒數是10個RUNNING狀態的,佇列裡100個也滿了。如果這時候又進來1個請求,則直接走拒絕策略。

五、執行緒池的執行與關閉

5.1 執行任務

執行緒池中 submit()execute() 方法有什麼區別?

  • 接收引數:execute()只能執行 Runnable 型別的任務。submit()可以執行 Runnable 和 Callable 型別的任務。

  • 返回值:execute() ⽅法⽤於提交不需要返回值的任務,所以⽆法判斷任務是否被執行緒池執⾏成功與否;submit() ⽅法⽤於提交需要返回值的任務。執行緒池會返回⼀個 Future 型別的物件,通過這個 Future 物件可以判斷任務是否執⾏成功,並且可以通過 Future 的 get() ⽅法來獲取返回值, get() ⽅法會阻塞當前執行緒直到任務完成,⽽使⽤ get(long timeout,TimeUnit unit) ⽅法則會阻塞當前執行緒⼀段時間後⽴即返回,這時候有可能任務沒有執⾏完。

  • 異常處理:submit()方便Exception處理

5.2 關閉執行緒池

可以呼叫 shutdown()shutdownNow() 方法關閉執行緒池,原理是遍歷執行緒池中的工作執行緒,然後逐個呼叫執行緒的 interrupt 方法中斷執行緒,無法響應中斷的任務可能永遠無法終止。

二者區別

  • shutdownNow 首先將執行緒池的狀態設為 STOP,然後嘗試停止正在執行或暫停任務的執行緒,並返回等待執行任務的列表。
  • shutdown 只是將執行緒池的狀態設為 SHUTDOWN,然後中斷沒有正在執行任務的執行緒。通常呼叫 shutdown 來關閉執行緒池,如果任務不一定要執行完可呼叫 shutdownNow。

六、執行緒池執行緒數如何設計?即執行緒池執行緒數與(CPU密集型任務和I/O密集型任務)的關係

CPU密集型: 這種任務一般不佔用大量IO,所以後臺伺服器可以快速處理,壓力落在CPU上。

I/O密集型:常有大資料量的查詢和批量插入操作,此時的壓力主要在I/O上。

  • 與CPU密集型的關係:一般情況下,CPU核心數 == 最大同時執行執行緒數。在這種情況下(設CPU核心數為n),大量客戶端會發送請求到伺服器,但是伺服器最多隻能同時執行n個執行緒。所以這種情況下,無需設定過大的執行緒池工作佇列,(工作佇列長度 = CPU核心數 || CPU核心數+1)即可。

  • 與I/O密集型的關係:由於長時間的I/O操作,導致執行緒一直處於工作佇列,但它又不佔用CPU,則此時有1個CPU是處於空閒狀態的。所以,這種情況下,應該加大執行緒池工作佇列的長度,儘量不讓CPU空閒下來,提高CPU利用率。

一般說來,執行緒池的大小應該怎麼設定(執行緒池初始的預設核心執行緒數大小?)(其中 N為CPU的個數 )。

  • 如果是CPU密集型應用,則執行緒池大小設定為 N+1

  • 如果是IO密集型應用,則執行緒池大小設定為 2N+1


不足之處還請多多指正