高并發(fā)編程知識體系
1.問題
1所坯、什么是線程的交互方式?
2挂捅、如何區(qū)分線程的同步/異步芹助,阻塞/非阻塞?
3闲先、什么是線程安全状土,如何做到線程安全?
4伺糠、如何區(qū)分并發(fā)模型蒙谓?
5、何謂響應(yīng)式編程退盯?
6彼乌、操作系統(tǒng)如何調(diào)度多線程?
2.關(guān)鍵詞
同步渊迁,異步慰照,阻塞,非阻塞琉朽,并行毒租,并發(fā),臨界區(qū),競爭條件墅垮,指令重排惕医,鎖,amdahl,gustafson
3.全文概要
由于單機(jī)的性能上限原因我們才不得不發(fā)展分布式技術(shù)算色。那么話說回來抬伺,如果單機(jī)的性能沒能最大限度的榨取出來,就盲目的就建設(shè)分布式系統(tǒng)灾梦,那就有點(diǎn)本末倒置了峡钓。如果單機(jī)性能滿足的話,就不要折騰復(fù)雜的分布式架構(gòu)若河。如果說分布式架構(gòu)是宏觀上的性能擴(kuò)展能岩,那么高并發(fā)則是微觀上的性能調(diào)優(yōu)。本文將從線程的基礎(chǔ)理論談起萧福,逐步探究線程的內(nèi)存模型拉鹃,線程的交互,線程工具和并發(fā)模型的發(fā)展鲫忍。掃除關(guān)于并發(fā)編程的諸多模糊概念膏燕,從新構(gòu)建并發(fā)編程的層次結(jié)構(gòu)。
4.基礎(chǔ)理論
4.1基本概念
開始學(xué)習(xí)并發(fā)編程前饲窿,我們需要熟悉一些理論概念煌寇。既然我們要研究的是并發(fā)編程,那首先應(yīng)該對并發(fā)這個概念有所理解才是逾雄,而說到并發(fā)我們肯定要要討論一些并行阀溶。
并發(fā):一個處理器同時處理多個任務(wù)
并行:多個處理器或者是多核的處理器同時處理多個不同的任務(wù)
然后我們需要再了解一下同步和異步的區(qū)別:
同步:執(zhí)行某個操作開始后就一直等著按部就班的直到操作結(jié)束
異步:執(zhí)行某個操作后立即離開,后面有響應(yīng)的話再來通知執(zhí)行者
接著我們再了解一個重要的概念:
臨界區(qū):公共資源或者共享數(shù)據(jù)
由于共享數(shù)據(jù)的出現(xiàn)鸦泳,必然會導(dǎo)致競爭银锻,所以我們需要再了解一下:
阻塞:某個操作需要的共享資源被占用了,只能等待做鹰,稱為阻塞
非阻塞:某個操作需要的共享資源被占用了击纬,不等待立即返回,并攜帶錯誤信息回去钾麸,期待重試
如果兩個操作都在等待某個共享資源而且都互不退讓就會造成死鎖:
死鎖:參考著名的哲學(xué)家吃飯問題
饑餓:饑餓的哲學(xué)家等不齊筷子吃飯
活鎖:相互謙讓而導(dǎo)致阻塞無法進(jìn)入下一步操作更振,跟死鎖相反,死鎖是相互競爭而導(dǎo)致的阻塞
4.2并發(fā)級別
理想情況下我們希望所有線程都一起并行飛起來饭尝。但是CPU數(shù)量有限肯腕,線程源源不斷,總得有個先來后到钥平,不同場景需要的并發(fā)需求也不一樣实撒,比如秒殺系統(tǒng)我們需要很高的并發(fā)程度,但是對于一些下載服務(wù),我們需要的是更快的響應(yīng)知态,并發(fā)反而是其次的捷兰。所以我們也定義了并發(fā)的級別,來應(yīng)對不同的需求場景负敏。
阻塞:阻塞是指一個線程進(jìn)入臨界區(qū)后贡茅,其它線程就必須在臨界區(qū)外等待,待進(jìn)去的線程執(zhí)行完任務(wù)離開臨界區(qū)后其做,其它線程才能再進(jìn)去友扰。
無饑餓:線程排隊(duì)先來后到,不管優(yōu)先級大小庶柿,先來先執(zhí)行,就不會產(chǎn)生饑餓等待資源秽浇,也即公平鎖浮庐;相反非公平鎖則是根據(jù)優(yōu)先級來執(zhí)行,有可能排在前面的低優(yōu)先級線程被后面的高優(yōu)先級線程插隊(duì)柬焕,就形成饑餓
無障礙:共享資源不加鎖审残,每個線程都可以自有讀寫,單監(jiān)測到被其他線程修改過則回滾操作斑举,重試直到單獨(dú)操作成功搅轿;風(fēng)險就是如果多個線程發(fā)現(xiàn)彼此修改了,所有線程都需要回滾富玷,就會導(dǎo)致死循環(huán)的回滾中璧坟,造成死鎖
無鎖:無鎖是無障礙的加強(qiáng)版,無鎖級別保證至少有一個線程在有限操作步驟內(nèi)成功退出赎懦,不管是否修改成功雀鹃,這樣保證了多個線程回滾不至于導(dǎo)致死循環(huán)
無等待:無等待是無鎖的升級版,并發(fā)編程的最高境界励两,無鎖只保證有線程能成功退出黎茎,但存在低級別的線程一直處于饑餓狀態(tài),無等待則要求所有線程必須在有限步驟內(nèi)完成退出当悔,讓低級別的線程有機(jī)會執(zhí)行傅瞻,從而保證所有線程都能運(yùn)行,提高并發(fā)度盲憎。
4.3量化模型
首先嗅骄,多線程不意味著并發(fā),但并發(fā)肯定是多線程或者多進(jìn)程焙畔。我們知道多線程存在的優(yōu)勢是能夠更好的利用資源掸读,有更快的請求響應(yīng)。但是我們也深知一旦進(jìn)入多線程,附帶而來的是更高的編碼復(fù)雜度儿惫,線程設(shè)計(jì)不當(dāng)反而會帶來更高的切換成本和資源開銷澡罚。但是總體上我們肯定知道利大于弊,這不是廢話嗎肾请,不然誰還愿意去搞多線程并發(fā)程序留搔,但是如何衡量多線程帶來的效率提升呢,我們需要借助兩個定律來衡量铛铁。
Amdahl
S=1/(1-a+a/n)
其中隔显,a為并行計(jì)算部分所占比例,n為并行處理結(jié)點(diǎn)個數(shù)饵逐。這樣括眠,當(dāng)1-a=0時,(即沒有串行倍权,只有并行)最大加速比s=n掷豺;當(dāng)a=0時(即只有串行,沒有并行)薄声,最小加速比s=1当船;當(dāng)n→∞時,極限加速比s→ 1/(1-a)默辨,這也就是加速比的上限德频。
Gustafson
系統(tǒng)優(yōu)化某部件所獲得的系統(tǒng)性能的改善程度,取決于該部件被使用的頻率缩幸,或所占總執(zhí)行時間的比例壹置。
兩面列舉了這兩個定律來衡量系統(tǒng)改善后提升效率的量化指標(biāo),具體的應(yīng)用我們在下文的線程調(diào)優(yōu)會再詳細(xì)介紹桌粉。
5.內(nèi)存模型
宏觀上分布式系統(tǒng)需要解決的首要問題是數(shù)據(jù)一致性蒸绩,同樣,微觀上并發(fā)編程要解決的首要問題也是數(shù)據(jù)一致性铃肯。貌似我們搞了這么多年的斗爭都是在公關(guān)一致性這個世界性難題患亿。既然并發(fā)編程要從微觀開始,那么我們肯定要對CPU和內(nèi)存的工作機(jī)理有所了解押逼,尤其是數(shù)據(jù)在CPU和內(nèi)存直接的傳輸機(jī)制步藕。
5.1整體原則
探究內(nèi)存模型之前我們要拋出三個概念:
原子性
在32位的系統(tǒng)中,對于4個字節(jié)32位的Integer的操作對應(yīng)的JVM指令集映射到匯編指令為一個原子操作挑格,所以對Integer類型的數(shù)據(jù)操作是原子性咙冗,但是Long類型為8個字節(jié)64位,32位系統(tǒng)要分為兩條指令來操作漂彤,所以不是原子操作雾消。
對于32位操作系統(tǒng)來說灾搏,單次次操作能處理的最長長度為32bit,而long類型8字節(jié)64bit立润,所以對long的讀寫都要兩條指令才能完成(即每次讀寫64bit中的32bit)
可見性
線程修改變量對其他線程即時可見
有序性
串行指令順序唯一狂窑,并行線程直接指令可能出現(xiàn)不一致,也即是指令被重排了
而指令重排也是有一定原則(摘自《深入理解Java虛擬機(jī)第12章》):
程序次序規(guī)則:一個線程內(nèi)桑腮,按照代碼順序泉哈,書寫在前面的操作先行發(fā)生于書寫在后面的操作;
鎖定規(guī)則:一個unLock操作先行發(fā)生于后面對同一個鎖額lock操作破讨;
volatile變量規(guī)則:對一個變量的寫操作先行發(fā)生于后面對這個變量的讀操作丛晦;
傳遞規(guī)則:如果操作A先行發(fā)生于操作B,而操作B又先行發(fā)生于操作C提陶,則可以得出操作A先行發(fā)生于操作C烫沙;
線程啟動規(guī)則:Thread對象的start()方法先行發(fā)生于此線程的每個一個動作;
線程中斷規(guī)則:對線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生隙笆;
線程終結(jié)規(guī)則:線程中所有的操作都先行發(fā)生于線程的終止檢測斧吐,我們可以通過Thread.join()方法結(jié)束、Thread.isAlive()的返回值手段檢測到線程已經(jīng)終止執(zhí)行仲器;
對象終結(jié)規(guī)則:一個對象的初始化完成先行發(fā)生于他的finalize()方法的開始;
5.2邏輯內(nèi)存
我們談的邏輯內(nèi)存也即是JVM的內(nèi)存格局仰冠。JVM將操作系統(tǒng)提供的物理內(nèi)存和CPU緩存在邏輯分為堆乏冀,棧,方法區(qū)洋只,和程序計(jì)數(shù)器辆沦。在《從宏觀微觀角度淺析JVM虛擬機(jī)》 一文我們詳細(xì)介紹了JVM的內(nèi)存模型分布,并發(fā)編程我們主要關(guān)注的是堆棧的分配识虚,因?yàn)榫€程都是寄生在棧里面的內(nèi)存段肢扯,把棧里面的方法邏輯讀取到CPU進(jìn)行運(yùn)算。
5.3物理內(nèi)存
而實(shí)際的物理內(nèi)存包含了主存和CPU的各級緩存還有寄存器担锤,而為了計(jì)算效率蔚晨,CPU往往回就近從緩存里面讀取數(shù)據(jù)。在并發(fā)的情況下就會造成多個線程之間對共享數(shù)據(jù)的錯誤使用肛循。
5.4內(nèi)存映射
由于可能發(fā)生對象的變量同時出現(xiàn)在主存和CPU緩存中铭腕,就可能導(dǎo)致了如下問題:
線程修改的變量對外可見
讀寫共享變量時出現(xiàn)競爭資源
由于線程內(nèi)的變量對棧外是不可見的,但是成員變量等共享資源是競爭條件多糠,所有線程可見累舷,就會出現(xiàn)如下當(dāng)一個線程從主存拿了一個變量1修改后變成2存放在CPU緩存,還沒來得及同步回主存時夹孔,另外一個線程又直接從主存讀取變量為1被盈,這樣就出現(xiàn)了臟讀析孽。
現(xiàn)在我們弄清楚了線程同步過程數(shù)據(jù)不一致的原因,接下來要解決的目標(biāo)就是如何避免這種情況的發(fā)生只怎,經(jīng)過大量的探索和實(shí)踐袜瞬,我們從概念上不斷的革新比如并發(fā)模型的流水線化和無狀態(tài)函數(shù)式化,而且也提供了大量的實(shí)用工具尝盼。接下來我們從無到有吞滞,先了解最簡單的單個線程的一些特點(diǎn),弄清楚一個線程有多少能耐后盾沫,才能深刻認(rèn)識多個線程一起打交道會出現(xiàn)什么幺蛾子裁赠。
6.線程單元
6.1狀態(tài)
我們知道應(yīng)用啟動體現(xiàn)的就是靜態(tài)指令加載進(jìn)內(nèi)存,進(jìn)而進(jìn)入CPU運(yùn)算赴精,操作系統(tǒng)在內(nèi)存開辟了一段棧內(nèi)存用來存放指令和變量值佩捞,從而形成了進(jìn)程。而其實(shí)我們的JVM也就是一個進(jìn)程而且蕾哟,而線程是進(jìn)程的最小單位一忱,也就是說進(jìn)程是由很多個線程組成的。而由于進(jìn)程的上下文關(guān)聯(lián)的變量谭确,引用帘营,計(jì)數(shù)器等現(xiàn)場數(shù)據(jù)占用了打段的內(nèi)存空間,所以頻繁切換進(jìn)程需要整理一大段內(nèi)存空間來保存未執(zhí)行完的進(jìn)程現(xiàn)場逐哈,等下次輪到CPU時間片再恢復(fù)現(xiàn)場進(jìn)行運(yùn)算芬迄。這樣既耗費(fèi)時間又浪費(fèi)空間,所以我們才要研究多線程昂秃。畢竟由于線程干的活畢竟少禀梳,工作現(xiàn)場數(shù)據(jù)畢竟少,所以切換起來比較快而且暫用少量空間肠骆。而線程切換直接也需要遵守一定的法則算途,不然到時候把工作現(xiàn)場破壞了就無法恢復(fù)工作了。
線程狀態(tài)
我們先來研究線程的生命周期蚀腿,看看Thread類里面對線程狀態(tài)的定義就知道
public enum State { /**
* Thread state for a thread which has not yet started.
*/
NEW, /**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE, /**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED, /**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
*
*
*
*
*
*
*
A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called Object.wait()
* on an object is waiting for another thread to call
* Object.notify() or Object.notifyAll() on
* that object. A thread that has called Thread.join()
* is waiting for a specified thread to terminate.
*/
WAITING, /**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
*
*
*
*
*
*
*
*/
TIMED_WAITING, /**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;
}
生命周期
線程的狀態(tài):NEW嘴瓤,RUNNABLE,BLOCKED莉钙,WAITING纱注,TIMED_WAITING,TERMINATED胆胰。注釋也解釋得很清楚各個狀態(tài)的作用狞贱,而各個狀態(tài)的轉(zhuǎn)換也有一定的規(guī)則需要遵循的。
6.2動作
介紹完線程的狀態(tài)和生命周期蜀涨,接下來我了解的線程具備哪些常用的操作瞎嬉。首先線程也是一個普通的對象Thread蝎毡,所有的線程都是Thread或者其子類的對象。那么這個內(nèi)存對象被創(chuàng)建出來后就會放在JVM的堆內(nèi)存空間氧枣,當(dāng)我們執(zhí)行start()方法的時候沐兵,對象的方法體在棧空間分配好對應(yīng)的棧幀來往執(zhí)行引擎輸送指令(也即是方法體翻譯成JVM的指令集)便监。
線程操作
新建線程:new Thread()扎谎,新建一個線程對象,內(nèi)存為線程在棧上分配好內(nèi)存空間
啟動線程:start()烧董,告訴系統(tǒng)系統(tǒng)準(zhǔn)備就緒毁靶,只要資源允許隨時可以執(zhí)行我棧里面的指令了
執(zhí)行線程:run(),分配了CPU等計(jì)算資源逊移,正在執(zhí)行棧里面的指令集
停止線程(過時):stop()预吆,把CPU和內(nèi)存資源回收,線程消亡胳泉,由于太過粗暴拐叉,已經(jīng)被標(biāo)記為過時
線程中斷:
interrupt(),中斷是對線程打上了中斷標(biāo)簽扇商,可供run()里面的方法體接收中斷信號凤瘦,至于線程要不要中斷,全靠業(yè)務(wù)邏輯設(shè)計(jì)案铺,而不是簡單粗暴的把線程直接停掉
isInterrupt()廷粒,主要是run()方法體來判斷當(dāng)前線程是否被置為中斷
interrupted(),靜態(tài)方法红且,也是用戶判斷線程是否被置為中斷狀態(tài),同時判斷完將線程中斷狀態(tài)復(fù)位
線程休眠:sleep()涤姊,靜態(tài)方法暇番,線程休眠指定時間段,此間讓出CPU資源給其他線程思喊,但是線程依然持有對象鎖壁酬,其他線程無法進(jìn)入同步塊,休眠完成后也未必立刻執(zhí)行恨课,需要等到資源允許才能執(zhí)行
線程等待(對象方法):wait()舆乔,是Object的方法,也即是對象的內(nèi)置方法剂公,在同步塊中線程執(zhí)行到該方法時希俩,也即讓出了該對象的鎖,所以無法繼續(xù)執(zhí)行
線程通知(對象方法):notify(),notifyAll()纲辽,此時該對象持有一個或者多個線程的wait颜武,調(diào)用notify()隨機(jī)的讓一個線程恢復(fù)對象的鎖璃搜,調(diào)用notifyAll()則讓所有線程恢復(fù)對象鎖
線程掛起(過時):suspend(),線程掛起并沒有釋放資源鳞上,而是只能等到resume()才能繼續(xù)執(zhí)行
線程恢復(fù)(過時):resume()这吻,由于指令重排可能導(dǎo)致resume()先于suspend()執(zhí)行,導(dǎo)致線程永遠(yuǎn)掛起篙议,所以該方法被標(biāo)為過時
線程加入:join()唾糯,在一個線程調(diào)用另外一個線程的join()方法表明當(dāng)前線程阻塞知道被調(diào)用線程執(zhí)行結(jié)束再進(jìn)行,也即是被調(diào)用線程織入進(jìn)來
線程讓步:yield()鬼贱,暫停當(dāng)前線程進(jìn)而執(zhí)行別的線程移怯,當(dāng)前線程等待下一輪資源允許再進(jìn)行,防止該線程一直霸占資源吩愧,而其他線程餓死
線程等待:park()芋酌,基于線程對象的操作,較對象鎖更為精準(zhǔn)
線程恢復(fù):unpark(Thread thread)雁佳,對應(yīng)park()解鎖脐帝,為不可重入鎖
線程分組
為了管理線程,于是有了線程組的概念糖权,業(yè)務(wù)上把類似的線程放在一個ThreadGroup里面統(tǒng)一管理堵腹。線程組表示一組線程,此外星澳,線程組還可以包括其他線程組疚顷。線程組形成一個樹,其中除了初始線程組以外的每個線程組都有一個父線程禁偎。線程被允許訪問它自己的線程組信息腿堤,但不能訪問線程組的父線程組或任何其他線程組的信息。
守護(hù)線程
通常情況下如暖,線程運(yùn)行到最后一條指令后則完成生命周期笆檀,結(jié)束線程,然后系統(tǒng)回收資源盒至⌒锶鳎或者單遇到異常或者return提前返回枷遂,但是如果我們想讓線程常駐內(nèi)存的話数冬,比如一些監(jiān)控類線程怎燥,需要24小時值班的县爬,于是我們又創(chuàng)造了守護(hù)線程的概念委乌。
setDaemon()傳入true則會把線程一直保持在內(nèi)存里面,除非JVM宕機(jī)否則不會退出痪伦。
線程優(yōu)先級
線程優(yōu)先級其實(shí)只是對線程打的一個標(biāo)志耍鬓,但并不意味這高優(yōu)先級的一定比低優(yōu)先級的先執(zhí)行阔籽,具體還要看操作系統(tǒng)的資源調(diào)度情況。通常線程優(yōu)先級為5牲蜀,邊界為[1,10]笆制。
/**
* The minimum priority that a thread can have.
*/
public final static int MIN_PRIORITY = 1;/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5; /**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10;
本節(jié)介紹了線程單元的轉(zhuǎn)態(tài)切換和常用的一些操作方法。如果只是單線程的話涣达,其他都沒必要研究這些在辆,重頭戲在于多線程直接的競爭配合操作,下一節(jié)則重點(diǎn)介紹多個線程的交互需要關(guān)注哪些問題度苔。
本文主要將的數(shù)基于JAVA的傳統(tǒng)多線程并發(fā)模型匆篓,下面例牌給出知識體系圖。
視頻資料領(lǐng)取??? 微信:Nancy007001
原創(chuàng): 編程原理林振華