在Java併發編程中,如何擴展和優化線程池?

不要菜


在java中多線程並不陌生,在一定的範圍內,多線程數量的增加會明顯提升整個系統的吞吐性能,但是線程本身會極大的耗費內存空間,線程的頻繁創建和回收也極其佔用CPU資源,多線程甚至會拖垮整個服務!

所以,線程的利用必須掌握在一個度,太少的線程數可能會浪費CPU資源,而太高也極有可能反而降低整個應用性能;

線程池:基於使用多線程存在的問題,JDK提出了線程池技術,類似於數據庫連接池,都是保持池中部分線程活躍狀態,在需要使用線程的時候,直接從線程池中獲取,使用。當線程使用結束,就進行回收(直接放回池中等待,而不是GC),這樣就能避免了線程的頻繁創建和回收。

JAVA中的線程池:JDK提供了線程池框架Executor,幫助程序更好的管理線程。總的結構如下截圖:


比較常見的線程池對象獲取方式為:

①newSingleThreadExecutor():返回單線程的線程池,一個接一個的處理任務,線程異常的時候,會創建新的線程替代; ②newFixedThreadPool:在達到最大線程之前,有一個任務就創建一個線程,直到達到最大線程數量; ③newCachedThreadPool:動態的設置最合適的線程數量,最大為JVM能夠支持的大小; ④newScheduledThreadPool:指定線程數量,並週期性的執行任務; ⑤newSingleThreadScheduledExecutor:指定線程數量1個,並週期性的執行任務;

從源碼來看,上面幾種線程池底層都是封裝的ThreadPoolExecutor對象,查看源碼可知比較重要的屬性(對象)截圖如下:



定義了線程池中的線程數量,最大線程池數量,線程工廠(用於線程的創建),workQuere任務隊列,handler拒絕策略等屬性,用於線程池的對象初始化和任務調度!

下圖是ThreadPoolExecutor對象中的execute方法截圖:


解釋如下:

1,當前線程總數小於核心線程數,則通過addWorker進行執行;

2,否則通過wordQueue.offer提交到等待隊列,

3,進入等待隊列失敗,則通過addWorker提交到線程池,失敗則執行拒絕策略;

線程池有多種拒絕策略:直接拋出異常,或者丟棄無法處理的任務等等,此處不做詳細討論。。

線程池的擴展:JDK允許開發人員自主擴展線程池,通過提供的beforeExecute,afterExecute,terminated三個接口可以像處理AOP一樣方便的管理線程池,可自行實現狀態跟蹤,調試信息等用以監控線程池!

線程池的優化:線程池的優化主要針對線程數量進行,一般來說只要使用的不是最大最小線程數量都可以,但是具體的還要根據場景,參考CPU核心數,等待時間等因素來判斷最合適的線程數,比如是批量運算這種密集的CPU執行,則線程數設置為CPU核心數即可,如果有大量阻塞,則可以使用CPU核心數的偶數倍數,在有一本書中得出了一個公式如下截圖:

jdk中的線程池技術比較完善,加上其他的多線程技術,促使JAVA成為高併發領域的佼佼者,最近一直在分享JAVA技術,得到很多朋友的鼓勵,在此表示感謝,我也會一直持續的進行分享,敬請關注。。


此生唯一


筆者從事Java開發多年,在Java併發編程中積累了一定的經驗,就線程池擴展優化問題,來分享一下自己的一些看法。

線程池利用好了,可以高系統性能,如果利用不好,也會帶來一系列問題。今天我們從6個方面來談線程池的優化。

1. 核心線程WarmUp優化

默認情況下,核心工作線程值在初始的時候被創建,當新任務到來的時候被啟動,但是我們可以通過重寫prestartCoreThread或prestartCoreThreads方法來改變這種行為。通常情況下我們可以在應用啟動時來WarmUp核心線程,從而達到任務過來能夠立即執行的結果,使得初始任務處理的時間得到一定優化。

2. 定製工作線程的創建優化

新的線程是通過ThreadFactory來創建的,如果沒有指定,默認會使用Executors#defaultThreadFactory,這時創建的線程將都屬於同一個線程組,擁有同樣的優先級和daemon狀態。通過擴展配置ThreadFactory,我們可以配置線程的名稱、線程組合daemon狀態。如果調用ThreadFactory#createThread失敗,將返回null,executor將不會執行任何任務。

3. 核心線程回收

如果當前池子中的工作線程數大於corePoolSize,超過corePoolSize的線程處於空閒的時間大於keepAliveTime,則這些線程將會被終止,這是一種減少不必要資源消耗的策略。這個參數可以在運行時被改變,我們同樣可以將這種策略應用給核心線程,可以通過調用allowCoreThreadTimeout來實現。

4. 正確的選擇隊列

下面主要是不同隊列策略表現:

4.1 直接遞交:一種比較好的默認選擇是使用SynchronousQueue,這種策略會將提交的任務直接傳送給工作線程,而不持有。如果當前沒有工作線程來處理,即任務放入隊列失敗,則根據線程池的實現,引發新的工作線程創建,新提交的任務就會被處理。這種策略在當提交的一批任務之間有依賴關係時能避免鎖競爭消耗。值得一提的是,這種策略最好配合unbounded線程數來使用,從而避免任務被拒絕。同時我們必須要考慮到一種場景,當任務到來的速度大於任務處理的速度時,將會引起無限制的線程數不斷增加的問題。

4.2 無界隊列:使用無界隊列如LinkedBlockingQueue沒有指定最大容量時,將會引起當核心線程都在忙的時候,新的任務被放在隊列上,因此,永遠不會有大於corePoolSize的線程被創建,maximumPoolSize參數將失效。這種策略比較適合所有的任務都不相互依賴、獨立執行的情況。舉個例子,網頁服務器中,每個線程獨立處理請求。但是當任務處理速度小於任務進入速度時會引起隊列的無限膨脹。

4.3 有界隊列:有界隊列如ArrayBlockingQueue幫助限制資源的消耗,但是不好控制。隊列長度和maximumPoolSize這兩個值會相互影響,使用大的隊列和小maximumPoolSize會減少CPU的使用、操作系統資源、上下文切換的消耗,但是會降低吞吐量。如果任務被頻繁的阻塞,系統則可以調度更多的線程。使用小的隊列通常需要大maximumPoolSize,從而使得CPU更忙一些,但是又會增加降低吞吐量的線程調度的消耗。

總結一下是IO密集型可以考慮多些線程來平衡CPU的使用,CPU密集型可以考慮少些線程減少線程調度的消耗。

5. 合理的配置線程池

要想合理的配置線程池,就必須首先分析任務特性,可以從以下幾個角度來進行分析:

  • 任務性質:CPU密集型任務,IO密集型任務和混合型任務;

  • 任務優先級:高,中和低;

  • 任務執行時間:長,中和短;

  • 任務依賴性:是否依賴其他系統資源,如數據庫連接等。

任務性質不同的任務可以用不同規模的線程池分開處理。

  • CPU密集型任務配置儘可能小的線程,如配置Ncpu+1個線程的線程池。

  • IO密集型任務則由於線程並不是一直在執行任務,則配置儘可能多的線程,如2*Ncpu。

  • 混合型的任務,如果可以拆分,則將其拆分成一個CPU密集型任務和一個IO密集型任務,只要這兩個任務執行的時間相差不是太大,那麼分解後執行的吞吐率要高於串行執行的吞吐率,如果這兩個任務執行時間相差太大,則沒必要進行分解。我們可以通過Runtime.getRuntime().availableProcessors()方法獲得當前設備的CPU個數。

優先級不同的任務可以使用優先級隊列PriorityBlockingQueue來處理。它可以讓優先級高的任務先得到執行,需要注意的是如果一直有優先級高的任務提交到隊列裡,那麼優先級低的任務可能永遠不能執行。

執行時間不同的任務可以交給不同規模的線程池來處理,或者也可以使用優先級隊列,讓執行時間短的任務先執行。

依賴數據庫連接池的任務,因為線程提交SQL後需要等待數據庫返回結果,如果等待的時間越長CPU空閒時間就越長,那麼線程數應該設置越大,這樣才能更好的利用CPU。

建議使用有界隊列,有界隊列能增加系統的穩定性和預警能力。

6. 線程池的監控

通過線程池提供的參數進行監控。線程池裡有一些屬性在監控線程池的時候可以使用:

  • taskCount:線程池需要執行的任務數量。

  • completedTaskCount:線程池在運行過程中已完成的任務數量。小於或等於taskCount。

  • largestPoolSize:線程池曾經創建過的最大線程數量。通過這個數據可以知道線程池是否滿過。如等於線程池的最大大小,則表示線程池曾經滿了。

  • getPoolSize:線程池的線程數量。如果線程池不銷燬的話,池裡的線程不會自動銷燬,所以這個大小隻增不+ getActiveCount:獲取活動的線程數。

  • 通過擴展線程池進行監控。通過繼承線程池並重寫線程池的beforeExecute,afterExecute和terminated方法,我們可以在任務執行前,執行後和線程池關閉前幹一些事情。如監控任務的平均執行時間,最大執行時間和最小執行時間等。這幾個方法在線程池裡是空方法。如protected void beforeExecute(Thread t, Runnable r) { }

歡迎關注筆者,持續分享有價值的優質架構文章。


架構師成長錄


線程池創建和銷燬是有代價的,所以可以通過提前創建線程池來緩解這個問題。但是創建多少個是個問題?

一般根據業務複雜度,比如提前創建100個,然後設置一個低水位和高水位,比如20% 和80%,當達到低水位且持續一段時間,就可以釋放一部分。當高水位一段時間後,可以動態增加一部分。同時增加手動設置的api可以根據預測提前調整。


拉布拉斯


就是放線程的一個容器,每次創建線程很浪費系統資源。用的時候從線程池取,用完了就放回去就行了。


分享到:


相關文章: