幾種線程池的實現算法分析

1. 前言

在閱讀研究線程池的源碼之前,一直感覺線程池是一個框架中最高深的技術。研究後才發現,線程池的實現是如此精巧。本文從技術角度分析了線程池的本質原理和組成,同時分析了JDK、Jetty6、Jetty8、Tomcat的源碼實現,對於想了解線程池本質、更好的使用線程池或者定製實現自己的線程池的業務場景具有一定指導意義。

2. 使用線程池的意義

  • 複用:類似WEB服務器等系統,長期來看內部需要使用大量的線程處理請求,而單次請求響應時間通常比較短,此時Java基於操作系統的本地調用方式大量的創建和銷燬線程本身會成爲系統的一個性能瓶頸和資源浪費。若使用線程池技術可以實現工作線程的複用,即一個工作線程創建和銷燬的生命週期期間內可以執行處理多個任務,從而總體上降低線程創建和銷燬的頻率和時間,提升了系統性能。
  • 流控:服務器資源有限,超過服務器性能的過高併發設置反而成爲系統的負擔,造成CPU大量耗費於上下文切換、內存溢出等後果。通過線程池技術可以控制系統最大併發數和最大處理任務量,從而很好的實現流控,保證系統不至於崩潰。
  • 功能:JDK的線程池實現的非常靈活,並提供了很多功能,一些場景基於功能的角度會選擇使用線程池。

3. 線程池技術要點:

從內部實現上看,線程池技術可主要劃分爲如下6個要點實現:

圖1線程池技術要點

  • 工作者線程worker:即線程池中可以重複利用起來執行任務的線程,一個worker的生命週期內會不停的處理多個業務job。線程池“複用”的本質就是複用一個worker去處理多個job,“流控“的本質就是通過對worker數量的控制實現併發數的控制。通過設置不同的參數來控制worker的數量可以實現線程池的容量伸縮從而實現複雜的業務需求
  • 待處理工作job的存儲隊列:工作者線程workers的數量是有限的,同一時間最多隻能處理最多workers數量個job。對於來不及處理的job需要保存到等待隊列裏,空閒的工作者work會不停的讀取空閒隊列裏的job進行處理。基於不同的隊列實現,可以擴展出多種功能的線程池,如定製隊列出隊順序實現帶處理優先級的線程池、定製隊列爲阻塞有界隊列實現可阻塞能力的線程池等。流控一方面通過控制worker數控制併發數和處理能力,一方面可基於隊列控制線程池處理能力的上限。
  • 線程池初始化:即線程池參數的設定和多個工作者workers的初始化。通常有一開始就初始化指定數量的workers或者有請求時逐步初始化工作者兩種方式。前者線程池啓動初期響應會比較快但造成了空載時的少量性能浪費,後者是基於請求量靈活擴容但犧牲了線程池啓動初期性能達不到最優。
  • 處理業務job算法:業務給線程池添加任務job時線程池的處理算法。有的線程池基於算法識別直接處理job還是增加工作者數處理job或者放入待處理隊列,也有的線程池會直接將job放入待處理隊列,等待工作者worker去取出執行。
  • workers的增減算法:業務線程數不是持久不變的,有高低峯期。線程池要有自己的算法根據業務請求頻率高低調節自身工作者workers的數量來調節線程池大小,從而實現業務高峯期增加工作者數量提高響應速度,而業務低峯期減少工作者數來節省服務器資源。增加算法通常基於幾個維度進行:待處理工作job數、線程池定義的最大最小工作者數、工作者閒置時間。

線程池終止邏輯:應用停止時線程池要有自身的停止邏輯,保證所有job都得到執行或者拋棄。

4. 幾種線程池的實現細節

結合上面的技術點,列舉幾種線程池實現方式。

  • 工作者workers與待處理工作隊列實現方式舉例:

    實現

    工作者workers結構與併發保護

    待處理工作隊列結構

    JDK

    使用了HashSet來存儲工作者workers,通過可重入鎖ReentrantLock對其進行併發保護。每個worker都是一個Runnable接口。

    使用了實現接口BlockingQueue的阻塞隊列來存儲待處理工作job,並把隊列作爲構造函數參數,從而實現業務可以靈活的擴展定製線程池的隊列。業務也可使用JDK自身的同步阻塞隊列SynchronousQueue、有界隊列ArrayBlockingQueue、無界隊列LinkedBlockingQueue、優先級隊列PriorityBlockingQueue。

    Jetty6

    同樣使用了HashSet存儲工作者workers,通過synchronized一個對象進行HashSet的併發保護。每個工作者實際上是一個Thread的擴展。

    使用了數組存儲待處理的job對象Runnable。數組初始化容量爲_maxThreads個,使用變量_queued計算保存當前內部待處理job的個數即數組length。超過數組最大值時,擴大_maxThreads個容量,因此數組永遠夠用夠大,容量無界。同樣是用synchronized一個對象的方式實現同步。

    Jetty8

    使用了ConcurrentLinkedQueue存儲工作者workers,利用JDK基於CAS算法的實現提高了併發效率,同時也降低了線程池併發保護的複雜程度。針對隊列ConcurrentLinkedQueue無法保證size()實時性問題引入原子變量AtomicInteger統計工作者數量。

    與JDK相同實現,使用了基於接口BlockingQueue的阻塞隊列來存儲待處理工作job,也支持在線程池構造函數的參數中傳入隊列類型。同時,Jetty8內部默認未設置隊列類型場景可自動設置使用2種隊列:有界無法擴容的ArrayBlockingQueue及Jetty自身定製擴展實現的可擴容隊列BlockingArrayQueue。

    Tomcat

    基於JDK的ThreadPoolExecutors實現,複用JDK業務

    複用JDK業務

  • 線程池初始化與處理業務job算法舉例:

    實現

    線程池構造與工作者初始化

    處理業務job的算法

    JDK

    1. 基於多個構造參數實現靈活初始化,幾個核心參數如下:

    corePoolSize:核心工作者數

    maximumPoolSize:最大工作者數

    keepAliveTime:超過核心工作者數時閒置工作者的存活時間。

    workQueue:待處理job隊列,即前面提到的BlockingQueue接口。

    2. 默認初始化後不啓動工作者,等待有請求時才啓動。可以通過調用線程池接口提前啓動核心工作數個工作者線程,也可以啓動業務期望的多個工作者線程。

    1. 工作者workers數量低於核心工作者數corePoolSize時會優先創建一個工作者worker處理job,處理成功則返回。

    2. 工作者workers數量高於核心工作者數時會優先把job放入到待處理隊列,放入隊列成功時處理結束。

    3. 步驟2中入隊失敗會識別工作者數是否還小於最大工作者數maximumPoolsize,小於的話也會新創建一個工作者worker處理job。

    4. 拒絕處理

    Jetty6

    1. 同樣支持設置多個參數:

    _spawnOrShrinkAt:擴容/縮容閥值

    _minThreads:最小工作者數

    _maxThreads:最大工作者數

    _maxIdleTimeMs:閒置工作者最大閒置超時時間

    2. 初始化後直接啓動_minThreads個工作者線程

    1. 查找閒置的工作者worker,找到則派發job。

    2. 沒有閒置的工作者,將job存入待處理數組。

    3. 當識別到數組中待處理job超過擴容閥值參數時,擴容增加工作者處理job

    4. 否則不處理

    Jetty8

    1. 配置參數類似Jetty6,去除了_spawnOrShrinkAt閥值參數。

    2. 初始化後直接啓動_minThreads個工作者線程

    非常簡單,直接將待處理job入隊。

    Tomcat

    1. 基於JDK線程池的構造方法

    2. 來請求時啓動工作者

    處理方法複用JDK的,但是在開始提交前擴展了JDK的功能,實現了可以統計提交數submittedCount的能力

  • 線程池工作者worker的增減機制舉例:

    實現

    工作者增加算法

    工作者減少算法

    JDK

    1. 待處理job來時,工作者workers數量低於核心工作者數corePoolSize時。

    2. 待處理job來時,workers數超過核心數小於最大工作者數且入待處理隊列失敗場景。

    3. 業務調用線程池的更新核心工作者數接口時,若發現擴容,會增加工作者數。

    1. 待處理任務隊列裏沒有job並且工作者workers數量超過了核心工作者數corePoolSize。

    2. 待處理任務隊列裏沒有job並且允許工作者數量小於核心工作者參數爲true,此場景會至少保留一個工作者線程。

    Jetty6

    1. 啓動線程池時會啓動_minThreads個工作者線程

    2. 待處理的job數量高於了閥值參數且工作者數沒有達到最大值時會增加工作者。

    3. 調用線程池接口setMinThreads更新最小工作者數時會根據需要增加工作者。

    如下三個條件同時滿足時會減少工作者:

    1. 待處理任務數組中沒有待處理job

    2. 工作者workers數量超過了最小工作者數_minThreads

    3. 閒置工作者線程數高於了閥值參數

    Jetty8

    1. 啓動線程池時啓動最小工作者參數個工作者線程

    2. 已經沒有閒置工作者或者閒置工作者的數量已經小於待處理的job的總數

    3. 調用線程池接口setMinThreads更新最小工作者數時

    如下三個條件同時滿足時會減少工作者:

    1. 待處理任務隊列裏沒有待處理的job

    2. 工作者workers總數超過了最小工作者參數配置_minThreads

    3. 工作者線程的閒置時間超時

    Tomcat

    同JDK增加工作者算法

    複用JDK減少算法,同時定製擴展延遲參數,超過參數時,直接拋出異常到外面來終止線程池工作者。

5. 小結

對比幾種線程池實現,JDK的實現是最爲靈活、功能最強且擴展性最好的,Tomcat即基於JDK線程池功能擴展實現,複用原有業務的同時擴充了自己的業務。Jetty6是完全自己定製的線程池業務,耦合線程池衆多複雜的業務邏輯到線程池類裏面,邏輯相對最爲複雜,擴展性也非常差。Jetty8相對Jetty6的實現簡化了很多,其中利用了JDK中的同步容器和原子變量,同時實現方式也越來越接近JDK。

6. 參考源碼

  • JDK源碼類:java.util.concurrent.ThreadPoolExecutor
  • Jetty6源碼類:org.mortbay.thread.QueuedThreadPool
  • Jetty8源碼類:org.eclipse.jetty.util.thread.QueuedThreadPool
  • Tomcat源碼類:org.apache.tomcat.util.threads.ThreadPoolExecutor
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章