10|Java執行緒(中):建立多少執行緒才是合適的?

java 領域實現併發程式的主要手段就是多執行緒,使用多執行緒很簡單,但是使用多少個執行緒卻是個難題,工作中經常有人問:各種執行緒池的數量多少合適呢?或者Tomcat的執行緒數、jdbc的執行緒數,我們該如何設定呢?

那麼先要搞清楚以下兩個問題:

為什麼要使用多執行緒?

使用多執行緒首要解決的就是提高效能,或者說快,但是 這個太籠統沒有具體衡量指標,所以我們看下如何衡量效能。

度量效能指標很多,核心的兩個:延遲量、吞吐量。

延遲指的就是發出請求到響應請求的時間,當然時間越短越好。

吞吐量單位時間內能夠處理的請求量,當然越多越好。

所以我們需要做的就是降低延遲、提高吞吐量。這也是我們使用多執行緒的目的。

多執行緒的應用場景

那麼我們如何做呢?主要有兩個方向:一是最佳化演算法 而是將硬體的效能發揮極致。前者屬於演算法領域,後者就和併發程式設計息息相關了。

硬體有哪些呢?主要是兩類:一個是I/O,一個是 CPU。 簡言之,

在併發程式設計領域,提升效能本質上就是提升硬體的利用率,再具體來說,就是提升I/O 的利用率和 CPU 的利用率

硬體方面,難道作業系統做的不完善嗎?是的,作業系統硬體的最佳化往往是針對單一的硬體裝置。而我們的併發程式往往需要CPU 和 I/O 裝置相互配合工作,所以 我們做的就是需要提高

CPU 和 I/O 裝置綜合利用率的問題

關於這個綜合利用率,作業系統雖然沒有辦法完美解決,但是卻給我們提供瞭解決方案。:多執行緒

下面我們透過一個簡單的示例:如何利用多執行緒來提升 CPU 和 I/O 裝置的利用率? 假設程式按照 CPU 計算和 I/O 操作交叉執行的方式執行,而且 CPU 計算和 I/O 操作的耗時是 1:1。

如下圖所示,我們只有一個執行緒,執行CPU 計算的時候, I/O 裝置空閒; 執行 I/O 操作的時候, CPU 空閒, 所以 CPU 的利用率和 I/O 裝置的利用率都是 50%。

10|Java執行緒(中):建立多少執行緒才是合適的?

如果有兩個執行緒,如下圖所示,當執行緒A 執行 CPU 計算的時候, 執行緒 B 執行 I/O 操作; 當執行緒 A 執行 I/O 操作的時候,執行緒 B 執行 CPU 計算,這樣 CPU 的利用率和 I/O 裝置的利用率就都達到了 100%。

10|Java執行緒(中):建立多少執行緒才是合適的?

我們將 CPU 的利用率和 I/O 裝置的利用率都提升到了 100%, 會對效能產生了哪些影響呢?透過上面圖示,很容易看出:單位時間處理的請求數量翻了一番,也就是說吞吐量提高了一倍。此時可以你想思維以下,

如果 CPU 和 I/O 裝置的利用率都很低,那麼可以嘗試透過增加執行緒來提高吞吐量。

在單核時代,多執行緒主要用來平衡CPU 和 I/O 裝置的。 如果程式只有CPU 計算, 而沒有I/O 操作的話, 多執行緒不但不會提升效能,還會使效能變得更差,原因是增加了執行緒切換成本。但是在多核時代,這種純計算型的程式可以利用多執行緒來提高效能。為什麼呢?因為利用多核可以降低響應時間。

為了便於你理解,這裡我舉個簡單的例子說明一下:計算 1+2+… … +100 億的值,如果在 4 核的 CPU 上利用 4 個執行緒執行,執行緒 A 計算 [1,25 億),執行緒 B 計算 [25 億,50 億),執行緒 C 計算 [50,75 億),執行緒 D 計算 [75 億,100 億],之後彙總,那麼理論上應該比一個執行緒計算 [1,100 億] 快將近 4 倍,響應時間能夠降到 25%。一個執行緒,對於 4 核的 CPU,CPU 的利用率只有 25%,而 4 個執行緒,則能夠將 CPU 的利用率提高到 100%。

10|Java執行緒(中):建立多少執行緒才是合適的?

建立多少執行緒合適?

建立多少合適,需要看具體的應用場景,我們程式一般是CPU 計算和 I/O 操作交叉執行的,由於 I/O 裝置的速度相對於 CPU 來說都很慢,所以大部分情況下,I/O 操作執行的時間相對於 CPU 計算來說都非常長,這種場景我們一般都稱為 I/O 密集型計算;

和 I/O 密集型計算相對的就是 CPU 密集型計算了,CPU 密集型計算大部分場景下都是純 CPU 計算。I/O 密集型程式和 CPU 密集型程式,計算最佳執行緒數的方法是不同的。

對於 CPU 密集型計算,多執行緒本質上是提升多核 CPU 的利用率, 所以對於一個4 核的 CPU,每個核一個執行緒,理論上建立 4 個執行緒就可以了, 再多建立執行緒只是增加執行緒切換的成本。所以,

對於 CPU 密集型的計算場景,理論上“執行緒的數量 =CPU 核數”就是最合適的

。不過在工程上,執行緒的數量一般會設定為“CPU 核數 +1”, 這樣的話,當執行緒因為偶爾的記憶體頁失效,或者其他原因導致阻塞,這個額外的執行緒可以頂上,從而保證CPU的利用率。

對於I/O 密集型的計算場景,比如前面我們提到的例子中,如果 CPU 計算和 I/O 操作的耗時是 1:1,那麼 2 個執行緒是最合適的。 如果 CPU 計算和 I/O 操作的耗時是 1:2,那多少個執行緒合適呢?是 3 個執行緒,如下圖所示:CPU 在 A、B、C 三個執行緒之間切換,對於執行緒 A,當 CPU 從 B、C 切換回來時,執行緒 A 正好執行完 I/O 操作。這樣 CPU 和 I/O 裝置的利用率都達到了 100%。

10|Java執行緒(中):建立多少執行緒才是合適的?

透過上面的例子,我們就會發現,對於I/O 密集型計算場景, 最佳的執行緒數是與程式中CPU 計算和 I/O 操作的耗時比相關的,我們可以總結出這樣一個公式:

最佳執行緒數 =1 +(I/O 耗時 / CPU 耗時)

我們令 R=I/O 耗時 / CPU 耗時,綜合上圖,可以這樣理解:當執行緒 A 執行 IO 操作時,另外 R 個執行緒正好執行完各自的 CPU 計算。這樣 CPU 的利用率就達到了 100%。

不過上面這個公式,是針對單核CPU 的,至於多核CPU ,也很簡單,只要等比擴大就可以了,計算公式如下:

最佳執行緒數 =CPU 核數 * [ 1 +(I/O 耗時 / CPU 耗時)]

總結

很多人都知道不是執行緒越多越好,但是設定多少合適又拿不定注意,其實只要把我一條原則就可以了,這條原子就是將硬體效能發揮極致,上面我們針對CPU密集型和I/O 密集型計算場景都給出了理論上的最佳公式,這些公式背後的目標其實就是將硬體的效能發揮到極致。

對於 I/O 密集型計算場景,I/O 耗時和 CPU 耗時的比值是一個關鍵引數,不幸的是這個引數是未知的,而且是動態變化的,所以工程上,我們要估算這個引數,然後做各種不同場景下的壓測來驗證我們的估計。不過工程上,原則還是將硬體的效能發揮到極致,所以壓測時,我們需要重點關注 CPU、I/O 裝置的利用率和效能指標(響應時間、吞吐量)之間的關係。