在高並發的情況下采用線程池,有效的降低了線程創建釋放的時間花銷及資源開銷,如不使用線程池,有可能造成系統創建大量線程而導致消耗完系統內存以及”過度切換”。(在JVM中采用的處理機制為時間片輪轉,減少了線程間的相互切換)
那么在高並發的情況下,我們怎么選擇最優的線程數量呢?選擇原則又是什么呢?這個問題去哪網的技術總監問過我,這里總結一下。
第一種:
如果是CPU密集型應用,則線程池大小設置為N+1;(對於計算密集型的任務,在擁有N個處理器的系統上,當線程池的大小為N+1時,通常能實現最優的效率。(即使當計算密集型的線程偶爾由於缺失故障或者其他原因而暫停時,這個額外的線程也能確保CPU的時鍾周期不會被浪費。摘自《Java Concurrency In Practise》)
如果是IO密集型應用,則線程池大小設置為2N+1
任務一般可分為:CPU密集型、IO密集型、混合型,對於不同類型的任務需要分配不同大小的線程池。CPU密集型任務 盡量使用較小的線程池,一般為CPU核心數+1。 因為CPU密集型任務使得CPU使用率很高,若開過多的線程數,只能增加上下文切換的次數,因此會帶來額外的開銷。IO密集型任務 可以使用稍大的線程池,一般為2*CPU核心數。 IO密集型任務CPU使用率並不高,因此可以讓CPU在等待IO的時候去處理別的任務,充分利用CPU時間。混合型任務 可以將任務分成IO密集型和CPU密集型任務,然后分別用不同的線程池去處理。 只要分完之后兩個任務的執行時間相差不大,那么就會比串行執行來的高效。 因為如果划分之后兩個任務執行時間相差甚遠,那么先執行完的任務就要等后執行完的任務,最終的時間仍然取決於后執行完的任務,而且還要加上任務拆分與合並的開銷,得不償失。
第二種呢,先由之前遇到的一個測試題說起,假設要求一個系統的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吞吐量加進去。在IO優化文檔中,有這樣地公式:
最佳線程數目 = ((線程等待時間+線程CPU時間)/線程CPU時間 )* CPU數目
即線程等待時間所占比例越高,需要越多線程。線程CPU時間所占比例越高,需要越少線程。
但根據短板效應,真實的系統吞吐量並不能單純根據CPU來計算。那要提高系統吞吐量,就需要從“系統短板”(比如網絡延遲、IO)着手:
盡量提高短板操作的並行化比率,比如多線程下載技術
增強短板能力,比如用NIO替代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和網絡操作。
總的來說,應用情況不同,采取多線程/單線程策略不同;線程池情況下,不同的估算,目的和出發點是一致的。
參考借鑒:http://ifeve.com/how-to-calculate-threadpool-size/
https://www.zhihu.com/question/38128980
相互學習,共同進步!