幾種線程池的實現算法分析【轉載】

 

原文地址html

本文內容

  • 前言
  • 線程池意義
  • 線程池技術要點
  • 小節
  • 參考源碼

但凡是一個框架(「服務」框架),基本都會涉及線程池問題。雖然你可能沒有直接使用它,但這是由於框架幫你完成了這部分工做。java

說,爲何須要線程池呢?試想,如今但凡是寫一個服務程序,若是不採用併發或並行的方式,都有點對不起4核、8核,甚至更多的CPU內核(物理內核,邏輯內核)。若是每次須要線程,都建立一次,使用完後,銷燬,對系統性能的消耗比較大,更加合適的作法是,在程序初始化時,一次性把全部線程都建立好,這樣,當須要的時候,直接用就行~python

雖然框架提供線程池,不須要本身寫,但瞭解線程池,至少能大幅度提升你的編程能力。可能,不是每一個項目都要搞一套框架,若是項目沒那麼複雜,本身寫個簡單的線程池仍是須要的。算法

遷移到:http://www.bdata-cap.com/newsinfo/1828966.htmlapache

前言


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

線程池意義


  • 複用:相似Web服務器等系統,內部須要使用大量的線程處理客戶端請求,而單次請求響應時間一般比較短,此時Java基於操做系統的本地調用方式大量的建立和銷燬線程自己會成爲系統的一個瓶頸和資源浪費。若使用線程池技術能夠實現工做線程的複用,即一個工做線程建立和銷燬的生命週期期間內能夠執行處理多個任務,從整體上下降線程建立和銷燬的頻率和時間,提高了系統性能。
  • 流控:服務器資源有限,超過服務器性能的太高併發設置反而成爲系統的負擔,形成CPU大量耗費於上下文切換、內存溢出等後果。經過線程池技術能夠控制系統最大併發數和最大處理任務量,從而很好的實現流控,保證系統不至於崩潰。
  • 功能:JDK的線程池實現的很是靈活,並提供了不少功能,一些場景基於功能的角度會選擇使用線程池。

線程池技術要點


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

0723010

圖1 線程池技術要點tomcat

  • 工做者線程worker

線程池中的線程能夠重複利用執行任務,一個worker在生命週期內會不停的處理多個任務job,本質上,就是複用一個worker去處理多個job服務器

「流控「本質是經過對worker數量的控制實現併發數控制。經過設置參數控制worker的數量,實現線程池的容量伸縮。併發

  • 待處理job的存儲隊列

工做者線程workers的數量是有限的,同一時間最多隻能處理workers數量個job。對於來不及處理的job須要保存到等待隊列,空閒的工做者worker會不停的讀取隊列裏的job進行處理。

基於不一樣的隊列,能夠實現多種線程池,如定製隊列出隊順序實現帶處理優先級的線程池、定製隊列爲阻塞有界隊列實現可阻塞能力的線程池等。

流控一方面經過控制worker數控制併發數和處理能力,一方面可基於隊列控制線程池處理能力的上限。

  • 線程池初始化

線程池參數的設定和多個工做者workers的初始化。一般有一開始就初始化指定數量的workers或者有請求時逐步初始化工做者兩種方式。前者線程池啓動初期響應會比較快但形成了空載時的少許性能浪費,後者是基於請求量靈活擴容但犧牲了線程池啓動初期性能達不到最優。

  • 處理業務job算法

業務給線程池添加任務job時線程池的處理算法。有的線程池基於算法識別直接處理job,仍是增長工做者數量來處理job,或者放入待處理隊列,也有的線程池會直接將job放入待處理隊列,等待工做者worker去取出執行。

  • workers增減算法

業務線程數不是持久不變的,有高低峯期。線程池要有本身的算法根據業務請求頻率高低調節自身工做者workers的數量,從而調節線程池大小,實現業務高峯期增長工做者數量提升響應速度,而業務低峯期減小工做者數來節省服務器資源。增長算法一般基於幾個維度進行:待處理工做job數、線程池定義的最大最小工做者數、工做者閒置時間。

  • 線程池終止邏輯

中止時線程池要有自身的中止邏輯,保證全部job都獲得執行或者拋棄。

幾種線程池的實現


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

工做者workers與待處理工做隊列實現方式舉例

表 1

實現

工做者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算法舉例

表 2

實現

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

處理業務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的增減機制舉例

表 3

實現

工做者增長算法

工做者減小算法

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減小算法,同時定製擴展延遲參數,超過參數時,直接拋出異常到外面來終止線程池工做者。

小結


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

參考源碼


  • JDK源碼類:java.util.concurrent.ThreadPoolExecutor
  • Jetty6源碼類:org.mortbay.thread.QueuedThreadPool
  • Jetty8源碼類:org.eclipse.jetty.util.thread.QueuedThreadPool
  • Tomcat源碼類:org.apache.tomcat.util.threads.ThreadPoolExecutor
  • python 線程池
  • Quartz.Net SimpleThreadPool 和 ZeroSizeThreadPool
相關文章
相關標籤/搜索