1. 程式人生 > >面試問我,建立多少個執行緒合適?我該怎麼說

面試問我,建立多少個執行緒合適?我該怎麼說

| **如果好看,請給個贊** > - 你有一個思想,我有一個思想,我們交換後,一個人就有兩個思想 > > - If you can NOT explain it simply, you do NOT understand it well enough 現陸續將Demo程式碼和技術文章整理在一起 [Github實踐精選](https://github.com/FraserYu/learnings) ,方便大家閱讀檢視,本文同樣收錄在此,覺得不錯,還請Star ![](https://img2020.cnblogs.com/other/1583165/202004/1583165-20200408081625926-1357855565.png) ## 為什麼要使用多執行緒? > 防止併發程式設計出錯最好的辦法就是不寫併發程式 ![](https://img2020.cnblogs.com/other/1583165/202004/1583165-20200408081626228-1546375759.gif) 既然多執行緒程式設計容易出錯,為什麼它還經久不衰呢? **A:那還用說,肯定在某些方面有特長唄,比如你知道的【它很快,非常快】** 我也很贊同這個答案,但說的不夠具體 ## 併發程式設計適用於什麼場景? 如果問你選擇多執行緒的原因就是一個【快】字,面試也就不會出那麼多么蛾子了。你有沒有問過你自己 1. 併發程式設計在所有場景下都是快的嗎? 2. 知道它很快,何為快?怎樣度量? 想知道這兩個問題的答案,我們需要一個從【定性】到【定量】的分析過程 > 使用多執行緒就是在正確的場景下通過設定正確個數的執行緒來最大化程式的執行速度(我感覺你還是啥也沒說) 將這句話翻譯到硬體級別就是要充分的利用 CPU 和 I/O 的利用率 ![](https://img2020.cnblogs.com/other/1583165/202004/1583165-20200408081626609-1913685954.png) 兩個正確得到保證,也就能達到最大化利用 CPU 和 I/O的目的了。最關鍵是,如何做到兩個【正確】? 在聊具體場景的時候,我們必須要拿出我們的專業性來。送你兩個名詞 buff 加成 - CPU 密集型程式 - I/O 密集型程式 ### CPU 密集型程式 > 一個完整請求,I/O操作可以在很短時間內完成, CPU還有很多運算要處理,也就是說 CPU 計算的比例佔很大一部分 假如我們要計算 1+2+....100億 的總和,很明顯,這就是一個 CPU 密集型程式 在【單核】CPU下,如果我們建立 4 個執行緒來分段計算,即: 1. 執行緒1計算 `[1,25億)` 2. ...... 以此類推 3. 執行緒4計算 `[75億,100億]` 我們來看下圖他們會發生什麼? ![](https://img2020.cnblogs.com/other/1583165/202004/1583165-20200408081627115-2062930188.png) 由於是單核 CPU,所有執行緒都在等待 CPU 時間片。按照理想情況來看,四個執行緒執行的時間總和與一個執行緒5獨自完成是相等的,實際上我們還忽略了四個執行緒上下文切換的開銷 **所以,單核CPU處理CPU密集型程式,這種情況並不太適合使用多執行緒** 此時如果在 4 核CPU下,同樣建立四個執行緒來分段計算,看看會發生什麼? ![](https://img2020.cnblogs.com/other/1583165/202004/1583165-20200408081627622-1660033389.png) 每個執行緒都有 CPU 來執行,並不會發生等待 CPU 時間片的情況,也沒有執行緒切換的開銷。理論情況來看效率提升了 4 倍 **所以,如果是多核CPU 處理 CPU 密集型程式,我們完全可以最大化的利用 CPU 核心數,應用併發程式設計來提高效率** ### I/O密集型程式 > 與 CPU 密集型程式相對,一個完整請求,CPU運算操作完成之後還有很多 I/O 操作要做,也就是說 I/O 操作佔比很大部分 我們都知道在進行 I/O 操作時,CPU是空閒狀態,所以我們要最大化的利用 CPU,不能讓其是空閒狀態 同樣在單核 CPU 的情況下: ![](https://img2020.cnblogs.com/other/1583165/202004/1583165-20200408081628359-1320446911.png) 從上圖中可以看出,每個執行緒都執行了相同長度的 CPU 耗時和 I/O 耗時,如果你將上面的圖多畫幾個週期,CPU操作耗時固定,將 I/O 操作耗時變為 CPU 耗時的 3 倍,你會發現,CPU又有空閒了,這時你就可以新建執行緒 4,來繼續最大化的利用 CPU。 綜上兩種情況我們可以做出這樣的總結: > **執行緒等待時間所佔比例越高,需要越多執行緒;執行緒CPU時間所佔比例越高,需要越少執行緒。** 到這裡,相信你已經知道第一個【正確】使用多執行緒的場景了,那建立多少個執行緒是正確的呢? ## 建立多少個執行緒合適? 面試如果問到這個問題,這可是對你理論和實踐的統考。想完全答對,你必須要【精通/精通/精通】**小學算術** 從上面知道,我們有 CPU 密集型和 I/O 密集型兩個場景,不同的場景當然需要的執行緒數也就不一樣了 ### CPU 密集型程式建立多少個執行緒合適? 有些同學早已經發現,對於 CPU 密集型來說,理論上 `執行緒數量 = CPU 核數(邏輯)` 就可以了,但是實際上,數量一般會設定為 `CPU 核數(邏輯)+ 1`, 為什麼呢? 《Java併發程式設計實戰》這麼說: > 計算密(CPU)集型的執行緒恰好在某時因為發生一個頁錯誤或者因其他原因而暫停,剛好有一個“額外”的執行緒,可以確保在這種情況下CPU週期不會中斷工作。 所以對於CPU密集型程式, `CPU 核數(邏輯)+ 1` 個執行緒數是比較好的經驗值的原因了 ### I/O密集型程式建立多少個執行緒合適? 上面已經讓大家按照圖多畫幾個週期(你可以動手將I/O耗時與CPU耗時比例調大,比如6倍或7倍),這樣你就會得到一個結論,對於 I/O 密集型程式: > 最佳執行緒數 = `(1/CPU利用率)` = `1 + (I/O耗時/CPU耗時)` 我這麼體貼,當然擔心有些同學不理解這個公式,我們將上圖的比例手動帶入到上面的公式中: ![](https://img2020.cnblogs.com/other/1583165/202004/1583165-20200408081629234-1893627689.png) 這是一個CPU核心的最佳執行緒數,如果多個核心,那麼 I/O 密集型程式的最佳執行緒數就是: > 最佳執行緒數 = `CPU核心數` * `(1/CPU利用率)` = `CPU核心數` * `1 + (I/O耗時/CPU耗時)` 說到這,有些同學可能有疑問了,要計算 I/O 密集型程式,是要知道 CPU 利用率的,如果我不知道這些,那要怎樣給出一個初始值呢? 按照上面公式,假如幾乎全是 I/O耗時,所以純理論你就可以說是 **2N(N=CPU核數),當然也有說 2N + 1的**,(我猜這個 1 也是 backup),沒有找到具體的推倒過程,在【併發程式設計實戰-8.2章節】截圖在此,大家有興趣的可以自己看看 ![](https://img2020.cnblogs.com/other/1583165/202004/1583165-20200408081629584-1820856617.png) **理論上來說,理論上來說,理論上來說**,這樣就能達到 CPU 100% 的利用率 如果理論都好用,那就用不著實踐了,也就更不會有調優的事出現了。**不過在初始階段,我們確實可以按照這個理論之作為偽標準, 畢竟差也可能不會差太多,這樣調優也會更好一些** 談完理論,咱們說點實際的,公式我看懂了(定性階段結束),但是我有兩個疑問: 1. 我怎麼知道具體的 I/O耗時和CPU耗時呢? 2. 怎麼檢視CPU利用率? 沒錯,我們需要定量分析了 幸運的是,我們並不是第一個吃螃蟹的仔兒,其實有很多 APM (Application Performance Manager)工具可以幫我們得到準確的資料,學會使用這類工具,也就可以結合理論,在調優的過程得到更優的執行緒個數了。我這裡簡單列舉幾個,具體使用哪一個,具體應用還需要你自己去調研選擇,受篇幅限制,暫不展開討論了 1. SkyWalking 2. CAT 3. zipkin 上面瞭解了基本的理論知識,那面試有可能問什麼?又可能會以怎樣的方式提問呢? ## 面試小問 ### 小問一 > 假設要求一個系統的 TPS(Transaction Per Second 或者 Task Per Second)至少為20,然後假設每個Transaction由一個執行緒完成,繼續假設平均每個執行緒處理一個Transaction的時間為4s **如何設計執行緒個數,使得可以在1s內處理完20個Transaction?** ![](https://img2020.cnblogs.com/other/1583165/202004/1583165-20200408081630138-2037074409.png) 但是,但是,這是因為沒有考慮到CPU數目。家裡又沒礦,一般伺服器的CPU核數為16或者32,如果有80個執行緒,那麼肯定會帶來太多不必要的執行緒上下文切換開銷(希望這句話你可以主動說出來),這就需要調優了,來做到最佳 balance ### 小問二 > 計算操作需要5ms,DB操作需要 100ms,對於一臺 8個CPU的伺服器,怎麼設定執行緒數呢? 如果不知道請拿三年級期末考試題重新做(今天晚自習留下來),答案是: **執行緒數 = 8 * (1 + 100/5) = 168 (個)** > 那如果DB的 QPS(Query Per Second)上限是1000,此時這個執行緒數又該設定為多大呢? ![](https://img2020.cnblogs.com/other/1583165/202004/1583165-20200408081630823-2038496281.png) 同樣,這是沒有考慮 CPU 數目,接下來就又是細節調優的階段了 因為一次請求不僅僅包括 CPU 和 I/O操作,具體的調優過程還要考慮記憶體資源,網路等具體內容 ## 增加 CPU 核數一定能解決問題嗎? 看到這,有些同學可能會認為,即便我算出了理論執行緒數,但實際CPU核數不夠,會帶來執行緒上下文切換的開銷,所以下一步就需要增加 CPU 核數,那我們盲目的增加 CPU 核數就一定能解決問題嗎? 在講互斥鎖的內容是,我故意遺留了一個知識: ### ![](https://img2020.cnblogs.com/other/1583165/202004/1583165-20200408081631315-749914814.png) 怎麼理解這個公式呢? ![](https://img2020.cnblogs.com/other/1583165/202004/1583165-20200408081631657-1435692358.png) 這個結論告訴我們,假如我們的序列率是 5%,那麼我們無論採用什麼技術,最高也就只能提高 20 倍的效能。 如何簡單粗暴的理解序列百分比(其實都可以通過工具得出這個結果的)呢?來看個小 Tips: > **Tips:** 臨界區都是序列的,非臨界區都是並行的,用單執行緒執行臨界區的時間/用單執行緒執行(臨界區+非臨界區)的時間就是序列百分比 現在你應該理解我在講解 synchronized 關鍵字時所說的: > 最小化臨界區範圍,因為臨界區的大小往往就是瓶頸問題的所在,不要像亂用try catch那樣一鍋端 ## 總結 多執行緒不一定就比但執行緒高效,比如大名鼎鼎的 Redis (後面會分析),因為它是基於記憶體操作,這種情況下,單執行緒可以很高效的利用CPU。而多執行緒的使用場景一般時存在相當比例的I/O或網路操作 另外,結合小學數學題,我們已經瞭解瞭如何從定性到定量的分析的過程,在開始沒有任何資料之前,我們可以使用上文提到的經驗值作為一個偽標準,其次就是結合實際來逐步的調優(綜合 CPU,記憶體,硬碟讀寫速度,網路狀況等)了 最後,盲目的增加 CPU 核數也不一定能解決我們的問題,這就要求我們嚴格的編寫併發程式程式碼了 ## 靈魂追問 1. 我們已經知道建立多少個執行緒合適了,為什麼還要搞一個執行緒池出來? 2. 建立一個執行緒都要做哪些事情?為什麼說頻繁的建立執行緒開銷很大? 3. 多執行緒通常要注意共享變數問題,為什麼區域性變數就沒有執行緒安全問題呢? 4. ...... 下一篇文章,我們就來說說,你熟悉又陌生的執行緒池問題 ## 參考 感謝前輩們總結的精華,自己所寫的併發系列好多都參考了以下資料 - Java 併發程式設計實戰 - Java 併發程式設計之美 - 碼出高效 - Java 併發程式設計的藝術 - ......