一、思考人生的多線程
我們一直在說高併發、多線程、分佈式應用,但是高併發情況下,多線程一定就快嗎?
我們首先要理解下併發運行是怎麼一回事。
為什麼一般意義上來說多線程就能抵抗高併發,運行速度就能得到提升?
所謂併發運行就是某個時間段CPU能執行多個任務。
例如早上起來後,刷牙、照鏡子、思考這復讀機一般的人生是為哪般?
但是我們真的能同時做這麼多事嗎?
不是的,其實是在大腦下達指令後,刷牙、照鏡子這種動作已經形成了肌肉記憶、固定動作,然後我們又有了幾分鐘思考人生的時光了。
同樣的道理放在計算上也是一樣。
我們首先要明白一個基礎知識,計算機的主要組件為CPU、內存、磁盤;而在這三大組件中,CPU的運行速率高於內存1000倍以上,內存的運行速率高於磁盤1000倍以上。
當然,這只是我的口嗨,並不是真的1000倍。
你只要知道他們的運行速率對比,CPU > 內存 > 磁盤就好了,並且要快很多。
二、應付多個嬰兒的哺乳媽媽
2.1 上下文切換
多線程執行就像是很多個嬰兒跟媽媽要奶喝一樣,怎麼辦?媽媽就兩個哺乳器官啊?最多就同時處理兩個嬰兒同時肚子餓的任務。
這已經是雙核CPU了。實際上,媽媽只能同時處理一個嬰兒哺乳的任務而已,沒辦法同時把兩個孩子抱在懷裡。你以為是母豬可以同時喂很多小豬呢?
所以只好讓嬰兒先喝點,抵抵飽,哄一鬨,然後換另外的孩子。
這就是CPU的【上下文切換】。
2.2 線程爭用
而很多時候,我們需要運行的任務並不是一樣的。
還是以嬰兒為例,嬰兒們同時大哭,要求佔用媽媽的時間,ABC嬰兒要換尿布、DEF嬰兒要喝奶。
怎麼辦?頭大。
這就是【線程爭用】。
2.3 併發執行
媽媽要開始安撫孩子了,但是隻能一個個來啊,要麼先找幾個奶瓶衝奶讓要喝奶的自己抱奶瓶喝奶,再去處理換尿布。
要麼先去處理尿布,再去衝奶。
衝奶這個動作很快,但是喝奶的時間很長。媽媽考慮了下,決定先衝奶,然後讓他們自己抱奶瓶。
這就是【併發執行】。
2.4 自旋鎖
但是衝奶的時候,嬰兒還在哭,等待著媽媽送來奶瓶和換尿布怎麼辦?
這就是【自旋鎖】。如果CPU一直不處理任務,就循環等待,直到CPU來處理。
2.5 互斥鎖
如果媽媽衝奶時,抱了一個嬰兒在懷裡哄著不哭,其他的嬰兒們沒指望了,就不哭了(當然,實際上不可能),等著媽媽空出手來又繼續哭,競爭媽媽的懷抱(笑)。
這就叫【互斥鎖】。它跟自旋鎖類似,不同的是競爭不到鎖的線程會回去睡會覺,等到鎖可用再來競爭。競爭失敗者繼續回去睡覺直到再次接到通知。
2.6 樂觀鎖
如果DEF拿到了奶瓶就不哭了,直到一瓶奶喝完還是喝不飽,才開始哭,要媽媽。這叫【樂觀鎖】。
樂觀鎖在數據庫的數據操作中,就是提交更新那一刻,才給相關數據行加鎖。
2.7 悲觀鎖
如果DEF拿到了奶瓶還是哭,因為他們還需要媽媽抱著喝才行。這叫【悲觀鎖】。
悲觀鎖就是如果一個事務操作用了鎖,那只有當這個事務把鎖釋放(把媽媽給釋放),其他事務才能夠執行與該鎖衝突的操作。
2.8 時間片分配算法
我們觀察以上的故事,可以發現它們並不是同時運行的。
而CPU相比媽媽來說,它的執行速度就更快了,它通過給每個線程分配CPU時間來實現任務運行,這個時間片一般是幾十毫秒。
這樣不停地來回切換任務,運行程序,劃分時間片,就叫做【時間片分配算法】。
2.9 線程與進程
上面說的是一個哺育室的故事,以JAVA而言,這就是一個【進程】。
如果媽媽還管著另一個哺育室,就又是一個進程。
一個哺育室有很多嬰兒,嬰兒們也可以認為是一個個線程。
所以【一個進程可以包含多個線程】。
而嬰兒們又享受著哺育室的公共環境與玩具。這就是【內存環境共享】。
在Java應用中,每個線程都是運行在進程的上下文中,共享【同樣的代碼和全局數據】。
如果其中一個哺育室的裝修風格、哺育規則變了,也不會影響到另一個哺育室。
所以在多進程環境中,任何一個進程的終止,都不會影響到其他進程。
所以在單核CPU時代,我們在一臺電腦上也可以同時聽歌、寫作,可以一邊看電影、一邊論壇灌水【多進程】,而一個論壇裡又可以有多個用戶同時灌水【多線程】。
三、多線程一定比單線程跑得快嗎?
回到最初的話題。
面試官:
如果有很多任務,每個任務需要CPU處理的時間都很長,佔用的時間片很高,那麼,多線程還能快嗎?
如果任務很少的情況下又是怎麼樣呢?
例如只有兩個嬰兒,來回換著哺乳快還是一個個餵飽來得快呢?
什麼情況下適合使用多線程呢?
除了CPU、內存、磁盤,還有什麼能影響併發執行的速度呢?
如果你是面試者,怎麼回答呢?留言給我說說看你的看法。
如果你對我上面的解釋並不滿意,有著不同的看法,也歡迎來噴一噴。
讀者福利:我把近一年經歷過的Java崗位面試,和一些刷過的面試題都做成了PDF,PDF都是可以免費分享給大家的,只要關注私信我:【101】,就能獲取免費領取方式!
閱讀更多 Java架構師丨蘇先生 的文章