Java併發(八)計算線程池最佳線程數

目錄

  1、理論分析java

  2、實際應用算法

 

爲了加快程序處理速度,咱們會將問題分解成若干個併發執行的任務。而且建立線程池,將任務委派給線程池中的線程,以便使它們能夠併發地執行。在高併發的狀況下采用線程池,能夠有效下降線程建立釋放的時間花銷及資源開銷,如不使用線程池,有可能形成系統建立大量線程而致使消耗完系統內存以及「過分切換」(在JVM中採用的處理機制爲時間片輪轉,減小了線程間的相互切換) 。數據庫

可是有一個很大的問題擺在咱們面前,即咱們但願儘量多地建立任務,但因爲資源所限咱們又不能建立過多的線程。那麼在高併發的狀況下,咱們怎麼選擇最優的線程數量呢?選擇原則又是什麼呢?編程

1、理論分析

關於如何計算併發線程數,有兩種說法。服務器

第一種,《Java Concurrency in Practice》即《java併發編程實踐》8.2節 170頁網絡

對於計算密集型的任務,一個有Ncpu個處理器的系統一般經過使用一個Ncpu + 1個線程的線程池來得到最優的利用率(計算密集型的線程剛好在某時由於發生一個頁錯誤或者因其餘緣由而暫停,恰好有一個「額外」的線程,能夠確保在這種狀況下CPU週期不會中斷工做)。對於包含了 I/O和其餘阻塞操做的任務,不是全部的線程都會在全部的時間被調度,所以你須要一個更大的池。爲了正確地設置線程池的長度,你必須估算出任務花在等待的時間與用來計算的時間的比率;這個估算值沒必要十分精確,並且能夠經過一些監控工具得到。你還能夠選擇另外一種方法來調節線程池的大小,在一個基準負載下,使用 幾種不一樣大小的線程池運行你的應用程序,並觀察CPU利用率的水平。多線程

  給定下列定義:併發

  Ncpu = CPU的數量高併發

  Ucpu = 目標CPU的使用率, 0 <= Ucpu <= 1工具

  W/C = 等待時間與計算時間的比率

  爲保持處理器達到指望的使用率,最優的池的大小等於:

  Nthreads = Ncpu x Ucpu x (1 + W/C)

  你可使用Runtime來得到CPU的數目:

int N_CPUS = Runtime.getRuntime().availableProcessors();

固然,CPU週期並非惟一你可使用線程池管理的資源。其餘能夠約束資源池大小的資源包括:內存、文件句柄、套接字句柄和數據庫鏈接等。計算這些類型資源池的大小約束很是簡單:首先累加出每個任務須要的這些資源的總童,而後除以可用的總量。所 得的結果是池大小的上限。

當任務須要使用池化的資源時,好比數據庫鏈接,那麼線程池的長度和資源池的長度會相互影響。若是每個任務都須要一個數據庫鏈接,那麼鏈接池的大小就限制了線程池的有效大小;相似地,當線程池中的任務是鏈接池的惟一消費者時,那麼線程池的大小反而又會限制了鏈接池的有效大小。

如上,在《Java Concurrency in Practice》一書中,給出了估算線程池大小的公式:

  Nthreads = Ncpu x Ucpu x (1 + W/C),其中

  Ncpu = CPU核心數

  Ucpu = CPU使用率,0~1

  W/C = 等待時間與計算時間的比率

第二種,《Programming Concurrency on the JVM Mastering》即《Java 虛擬機併發編程》2.1節 12頁

爲了解決上述難題,咱們但願至少能夠建立處理器核心數那麼多個線程。這就保證了有儘量多地處理器核心能夠投入到解決問題的工做中去。經過下面的代碼,咱們能夠很容易地獲取到系統可用的處理器核心數:

Runtime.getRuntime().availableProcessors();

因此,應用程序的最小線程數應該等於可用的處理器核數。若是全部的任務都是計算密集型的,則建立處理器可用核心數那麼多個線程就能夠了。在這種狀況下,建立更多的線程對程序性能而言反而是不利的。由於當有多個仟務處於就緒狀態時,處理器核心須要在線程間頻繁進行上下文切換,而這種切換對程序性能損耗較大。但若是任務都是IO密集型的,那麼咱們就須要開更多的線程來提升性能。

當一個任務執行IO操做時,其線程將被阻塞,因而處理器能夠當即進行上下文切換以便處理其餘就緒線程。若是咱們只有處理器可用核心數那麼多個線程的話,則即便有待執行的任務也沒法處理,由於咱們已經拿不出更多的線程供處理器調度了。

若是任務有50%的時間處於阻塞狀態,則程序所需線程數爲處理器可用核心數的兩倍。 若是任務被阻塞的時間少於50%,即這些任務是計算密集型的,則程序所需線程數將隨之減小,但最少也不該低於處理器的核心數。若是任務被阻塞的時間大於執行時間,即該任務是IO密集型的,咱們就須要建立比處理器核心數大幾倍數量的線程。

咱們能夠計算出程序所需線程的總數,總結以下:

  線程數 = CPU可用核心數/(1 - 阻塞係數),其中阻塞係數的取值在0和1之間。

  計算密集型任務的阻塞係數爲0,而IO密集型任務的阻塞係數則接近1。一個徹底阻塞的任務是註定要掛掉的,因此咱們無須擔憂阻塞係數會達到1。

爲了更好地肯定程序所需線程數,咱們須要知道下面兩個關鍵參數:

  • 處理器可用核心數;
  • 任務的阻塞係數;

第一個參數很容易肯定,咱們甚至能夠用以前的方法在運行時查到這個值。但肯定阻塞係數就稍微困難一些。咱們能夠先試着猜想,抑或採用一些性能分析工具或java.lang.management API來肯定線程花在系統IO操做上的時間與CPU密集任務所耗時間的比值。

如上,在《Programming Concurrency on the JVM Mastering》一書中,給出了估算線程池大小的公式:

  線程數 = Ncpu /(1 - 阻塞係數)

 

對於說法一,假設CPU 100%運轉,即撇開CPU使用率這個因素,線程數 = Ncpu x (1 + W/C)。

如今假設將方法二的公式等於方法一公式,即Ncpu /(1 - 阻塞係數)= Ncpu x (1 + W/C),推導出:阻塞係數 = W / (W + C),即阻塞係數 = 阻塞時間 /(阻塞時間 + 計算時間),這個結論在方法二後續中獲得印證,以下:

因爲對Web服務的請求大部分時間都花在等待服務器響應上了,因此阻塞係數會至關高,所以程序須要開的線程數多是處理器核心數的若干倍。假設阻塞係數是0.9,即每一個任務90%的時間處於阻塞狀態而只有10%的時間在幹活,則在雙核處理器上咱們就須要開20個線程(使用第2.1節的公式計算)。若是有不少只股票要處理的話,咱們能夠在8核處理器上開到80個線程來處理該任務。

因而可知,說法一和說法二實際上是一個公式。

2、實際應用

那麼實際使用中併發線程數如何設置呢?咱們先看一道題目:

假設要求一個系統的TPS(Transaction Per Second或者Task Per Second)至少爲20,而後假設每一個Transaction由一個線程完成,繼續假設平均每一個線程處理一個Transaction的時間爲4s。那麼問題轉化爲:

 

如何設計線程池大小,使得能夠在1s內處理完20個Transaction?

 

計算過程很簡單,每一個線程的處理能力爲0.25TPS,那麼要達到20TPS,顯然須要20/0.25=80個線程。

這個理論上成立的,可是實際狀況中,一個系統最快的部分是CPU,因此決定一個系統吞吐量上限的是CPU。加強CPU處理能力,能夠提升系統吞吐量上限。在考慮時須要把CPU吞吐量加進去。

分析以下(咱們以說法一公式爲例):

  Nthreads = Ncpu x (1 + W/C)

即線程等待時間所佔比例越高,須要越多線程。線程CPU時間所佔比例越高,須要越少線程。這就能夠劃分紅兩種任務類型:

IO密集型:通常狀況下,若是存在IO,那麼確定W/C > 1(阻塞耗時通常都是計算耗時的不少倍),可是須要考慮系統內存有限(每開啓一個線程都須要內存空間),這裏須要在服務器上測試具體多少個線程數適合(CPU佔比、線程數、總耗時、內存消耗)。若是不想去測試,保守點取1便可,Nthreads = Ncpu x (1 + 1) = 2Ncpu。這樣設置通常都OK。

計算密集型:假設沒有等待W = 0,則W/C = 0。 Nthreads = Ncpu

根據短板效應,真實的系統吞吐量並不能單純根據CPU來計算。那要提升系統吞吐量,就須要從「系統短板」(好比網絡延遲、IO)着手:

  • 儘可能提升短板操做的並行化比率,好比多線程下載技術;
  • 加強短板能力,好比用NIO替代IO;

第一條能夠聯繫到Amdahl定律,這條定律定義了串行系統並行化後的加速比計算公式:

加速比 = 優化前系統耗時 / 優化後系統耗時

加速比越大,代表系統並行化的優化效果越好。Addahl定律還給出了系統並行度、CPU數目和加速比的關係,加速比爲Speedup,系統串行化比率(指串行執行代碼所佔比率)爲F,CPU數目爲N:

Speedup <= 1 / (F + (1-F)/N)

當N足夠大時,串行化比率F越小,加速比Speedup越大。

這時候又拋出是否線程池必定比但線程高效的問題?

答案是否認的,好比Redis就是單線程的,但它卻很是高效,基本操做都能達到十萬量級/s。從線程這個角度來看,部分緣由在於:

  • 多線程帶來線程上下文切換開銷,單線程就沒有這種開銷;
  • 鎖;

固然「Redis很快」更本質的緣由在於: 

Redis基本都是內存操做,這種狀況下單線程能夠很高效地利用CPU。而多線程適用場景通常是:存在至關比例的IO和網絡操做。

總的來講,應用狀況不一樣,採起多線程/單線程策略不一樣;線程池狀況下,不一樣的估算,目的和出發點是一致的。

至此結論爲:

IO密集型 = 2Ncpu(能夠測試後本身控制大小,2Ncpu通常沒問題)(常出現於線程中:數據庫數據交互、文件上傳下載、網絡數據傳輸等等)

計算密集型 = Ncpu(常出現於線程中:複雜算法)

固然說法一中還有一種說法:

對於計算密集型的任務,一個有Ncpu個處理器的系統一般經過使用一個Ncpu + 1個線程的線程池來得到最優的利用率(計算密集型的線程剛好在某時由於發生一個頁錯誤或者因其餘緣由而暫停,恰好有一個「額外」的線程,能夠確保在這種狀況下CPU週期不會中斷工做)。

即,計算密集型 = Ncpu + 1,可是這種作法致使的多一個CPU上下文切換是否值得,這裏不考慮。讀者可本身考量。

相關文章
相關標籤/搜索