Java線程池詳解

一、合理使用線程池的好處

  • 降低資源消耗:通過重複利用已創建的線程降低線程創建和銷燬造成的消耗
  • 提高響應速度:任務不需要等待線程創建
  • 提高線程的可管理性:線程是稀缺資源,不能無限制地創建,使用線程池可以進行統一分配、調優和監控

二、線程池的實現原理

線程池的主要處理流程:

在這裏插入圖片描述

從圖中可以看出當提交一個新任務到線程池時,線程池的處理流程如下:

  1. 先判斷核心線程池(corePoolSize)裏面的線程是否已滿,如果未滿,則創建一個新的工作線程執行任務(執行時需要獲取全局鎖);如果滿了,則執行下一步驟。
  2. 判斷工作隊列(阻塞隊列)是否已滿,如果未滿,則將新任務添加到工作隊列;如果滿了,則進入下一步驟。
  3. 判斷線程池的最大線程數量(maximumPoolSize)是否達到,如果未達到,則創建一個新的工作線程來執行任務(執行時需要獲取全局鎖);如果達到,則交給飽和策略來處理這個任務。

上面提到了一個工作線程,什麼是工作線程?
線程池在創建線程時,會將線程封裝成工作線程(Worker),它在執行完任務後,還會循環獲取工作隊列裏的任務來執行(即不會完成任務後就消亡)。

執行示意圖:
在這裏插入圖片描述

我們發現,線程池是先把核心線程池填滿後,後面再有新任務則將任務添加進工作隊列,當工作隊列也滿的時候,這時線程池其實還沒有真正的滿,它還有(maximumPoolSize-corePoolSize)的容量(線程池的真正容量=工作隊列容量+maximumPoolSize),它會直接在覈心池外面創建線程直接處理任務。

那麼問題來了,線程池爲什麼要這麼設計?爲什麼不直接讓整個線程池填滿了再放進工作隊列中,而是隻填滿核心池就這樣做了?

原來線程池的設計思路是:爲了在執行execute()方法時,儘可能地避免獲取全局鎖,那樣會造成效率低下。

從上面的執行步驟可以知道,在覈心池創建新線程和在覈心池外面創建新線程執行任務時,都需要獲取全局鎖,而將任務加入工作隊列則不需要;在線程池完成預熱後(即將核心池的線程都創建起來成爲工作線程了),基本上所有的execute()方法都是執行的將任務加進工作隊列的操作,不需要獲取全局鎖,效率高。

三、線程池的使用

線程池的創建

之前舊的創建方式:

ExecutorService service = Executors.newFixedThreadPool(int nThreads);

用這種方式,在InteliJIDEA中使用阿里java規範插件會有個提示:
在這裏插入圖片描述

這種方式創建幾種不同的線程池參見:link

新的創建方式

//ThreadPoolExecutor構造函數
ThreadPoolExecutor(int corePoolSize,
                   int maximumPoolSize,
                   long keepAliveTime,
                   TimeUnit unit,
                   BlockingQueue<Runnable> workQueue)
                   
//創建方式
ThreadPoolExecutor pool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
  • corePoolSize(線程池的基本大小):當提交一個任務到線程池時,線程池會創建一個線程來執行任務,即使其他空閒的基本線程能夠執行新任務也會創建線程,等到需要執行的任務數大於線程池基本大小時就不再創建。如果調用了線程池的prestartAllCoreThreads()方法,線程池會提前創建並啓動所有基本線程。
  • maximumPoolSize(線程池最大數量):線程池允許創建的最大線程數。如果隊列滿了,並且已創建的線程數小於最大線程數,則線程池會再創建新的線程執行任務。如果使用了無界的工作隊列這個參數就沒什麼效果。
  • keepAliveTime(線程活動保持時間):線程池的工作線程空閒後,保持存活的時間。如果任務很多,並且每個任務執行的時間比較短,可以調大時間,提高線程的利用率。
  • TimeUnit(線程活動保持時間的單位):可選的單位有天(DAYS)、小時(HOURS)、分鐘(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和納秒(NANOSECONDS,千分之一微秒)。
  • runnableTaskQueue(工作隊列):用於保存等待執行的任務的阻塞隊列。可以選擇以下幾個阻塞隊列:
  1. ArrayBlockingQueue:是一個基於數組結構的有界阻塞隊列,FIFO(先進先出)原則。
  2. LinkedBlockingQueue:一個基於鏈表結構的阻塞隊列,FIFO原則,吞吐量通常要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()使用了這個隊列。
  3. SynchronousQueue:一個不存儲元素的阻塞隊列。每個插入操作必須等到另一個線程調用移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於LinkedBlockingQueue,靜態工廠方法Executors.newCachedThreadPool()使用了這個隊列。
  4. PriorityBlockingQueue:一個具有優先級的無限阻塞隊列。
  • RejectedExecutionHandler(飽和策略):當隊列和線程池都滿了,說明線程池處於飽和狀態,那麼必須採取一種策略處理提交的新任務。這個策略默認情況下是AbortPolicy,表示無法處理新任務時拋出異常。Java線程池框架提供了以下4種策略:(也可以實現該接口自定義策略,如記錄日誌)
  1. AbortPolicy:直接拋出異常。
  2. CallerRunsPolicy:只用調用者所在線程來運行任務。
  3. DiscardOldestPolicy:丟棄隊列裏最近的一個任務,並執行當前任務。
  4. DiscardPolicy:不處理,丟棄掉。

線程池提交任務

使用兩個方法向線程池提交任務,分別爲execute()submit()方法。

它們的區別

  • execute()方法用於提交不需要返回值的任務,所以無法判斷任務是否被線程池執行成功。
  • submit()方法用於提交需要返回值的任務。線程池會返回一個future類型的對象,通過這個future對象可以判斷任務是否執行成功,並且可以通過futureget()方法來獲取返回值,get()方法會阻塞當前線程直到任務完成,而使用get(long timeout,TimeUnit unit)方法則會阻塞當前線程一段時間後立即返回,這時候有可能任務沒有執行完。

關閉線程池

通過調用線程池的shutdownshutdownNow方法來關閉線程池,它們的原理是遍歷線程池中的工作線程,然後逐個調用線程的interrupt方法來中斷線程,所以無法響應中斷的任務可能永遠無法終止。

它們的區別

  • shutdownNow首先將線程池的狀態設置成STOP,然後嘗試停止所有的正在執行或暫停任務的線程,並返回等待執行任務的列表。
  • shutdown只是將線程池的狀態設置成SHUTDOWN,然後中斷所有沒有正在執行任務的線程,要等待已經執行的線程執行完。

注意

  • 只要調用了這兩個關閉方法中的任意一個,isShutdown方法就會返回true
  • 當所有的任務都已關閉後,才表示線程池關閉成功,這時調用isTerminaed方法會返回true

如何配置線程池

要合理配置線程池,需要從以下幾個角度分析任務特性:

  • 任務的性質:CPU密集型任務、IO密集型任務和混合型任務。
  • 任務的優先級:高、中和低。
  • 任務的執行時間:長、中和短。
  • 任務的依賴性:是否依賴其他系統資源,如數據庫連接。

配置規則

  • CPU密集型任務配置儘可能小的線程,如配置Ncpu+1個線程的線程池。
  • 由於IO密集型任務線程並不是一直在執行任務,則應配置儘可能多的線程,如2*Ncpu
  • 混合型的任務,如果可以拆分,將其拆分成一個CPU密集型任務和一個IO密集型任務,只要這兩個任務執行的時間相差不是太大,那麼分解後執行的吞吐量將高於串行執行的吞吐量。如果這兩個任務執行時間相差太大,則沒必要進行分解。
  • 優先級不同的任務可以使用優先級隊列PriorityBlockingQueue來處理。它可以讓優先級高的任務先執行(優先級低的任務可能永遠不能執行)。
  • 執行時間不同的任務可以交給不同規模的線程池來處理,或者可以使用優先級隊列,讓執行時間短的任務先執行
  • 依賴數據庫連接池的任務,因爲線程提交SQL後需要等待數據庫返回結果,等待的時間越長,則CPU空閒時間就越長,那麼線程數應該設置得越大,這樣才能更好地利用CPU
  • 建議使用有界隊列,有界隊列能增加系統的穩定性和預警能力,可以根據需要設大一點,比如幾千。

參考來源:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章