一個(gè)簡(jiǎn)單的單例示例
單例模式可能是大家經(jīng)常接觸和使用的一個(gè)設(shè)計(jì)模式稚伍,你可能會(huì)這么寫
publicclassUnsafeLazyInitiallization{
privatestaticUnsafeLazyInitiallizationinstance;
privateUnsafeLazyInitiallization(){
}
publicstaticUnsafeLazyInitiallizationgetInstance(){
if(instance==null){//1:A線程執(zhí)行
instance=newUnsafeLazyInitiallization();//2:B線程執(zhí)行
}
returninstance;
}
}
上面代碼大家應(yīng)該都知道贴届,所謂的線程不安全的懶漢單例寫法擂煞。在UnsafeLazyInitiallization類中,假設(shè)A線程執(zhí)行代碼1的同時(shí),B線程執(zhí)行代碼2,此時(shí)纤壁,線程A可能看到instance引用的對(duì)象還沒(méi)有初始化。
你可能會(huì)說(shuō)捺信,線程不安全酌媒,我可以對(duì)getInstance()方法做同步處理保證安全啊,比如下面這樣的寫法
publicclassSafeLazyInitiallization{
privatestaticSafeLazyInitiallizationinstance;
privateSafeLazyInitiallization(){
}
publicsynchronizedstaticSafeLazyInitiallizationgetInstance(){
if(instance==null){
instance=newSafeLazyInitiallization();
}
returninstance;
}
}
這樣的寫法是保證了線程安全迄靠,但是由于getInstance()方法做了同步處理秒咨,synchronized將導(dǎo)致性能開(kāi)銷。如getInstance()方法被多個(gè)線程頻繁調(diào)用掌挚,將會(huì)導(dǎo)致程序執(zhí)行性能的下降雨席。反之,如果getInstance()方法不會(huì)被多個(gè)線程頻繁的調(diào)用疫诽,那么這個(gè)方案將能夠提供令人滿意的性能。
那么旦委,有沒(méi)有更優(yōu)雅的方案呢奇徒?前人的智慧是偉大的,在早期的JVM中缨硝,synchronized存在巨大的性能開(kāi)銷摩钙,因此,人們想出了一個(gè)“聰明”的技巧——雙重檢查鎖定查辩。人們通過(guò)雙重檢查鎖定來(lái)降低同步的開(kāi)銷胖笛。下面來(lái)讓我們看看
publicclassDoubleCheckedLocking{//1
privatestaticDoubleCheckedLockinginstance;//2
privateDoubleCheckedLocking(){
}
publicstaticDoubleCheckedLockinggetInstance(){//3
if(instance==null){//4:第一次檢查
synchronized(DoubleCheckedLocking.class){//5:加鎖
if(instance==null)//6:第二次檢查
instance=newDoubleCheckedLocking();//7:問(wèn)題的根源出在這里
}//8
}//9
returninstance;//10
}//11
}
如上面代碼所示,如果第一次檢查instance不為null宜岛,那么就不需要執(zhí)行下面的加鎖和初始化操作长踊。因此,可以大幅降低synchronized帶來(lái)的性能開(kāi)銷萍倡。雙重檢查鎖定看起來(lái)似乎很完美身弊,但這是一個(gè)錯(cuò)誤的優(yōu)化!為什么呢列敲?在線程執(zhí)行到第4行阱佛,代碼讀取到instance不為null時(shí),instance引用的對(duì)象有可能還沒(méi)有完成初始化戴而。在第7行創(chuàng)建了一個(gè)對(duì)象凑术,這行代碼可以分解為如下的3行偽代碼
memory=allocate();//1:分配對(duì)象的內(nèi)存空間
ctorInstance(memory);//2:初始化對(duì)象
instance=memory;//3:設(shè)置instance指向剛分配的內(nèi)存地址
上面3行代碼中的2和3之間,可能會(huì)被重排序(在一些JIT編譯器上所意,這種重排序是真實(shí)發(fā)生的淮逊,如果不了解重排序催首,后文JMM會(huì)詳細(xì)解釋)。2和3之間重排序之后的執(zhí)行時(shí)序如下
memory=allocate();//1:分配對(duì)象的內(nèi)存空間
instance=memory;//3:設(shè)置instance指向剛分配的內(nèi)存地址壮莹,注意此時(shí)對(duì)象還沒(méi)有被初始化
ctorInstance(memory);//2:初始化對(duì)象
回到示例代碼第7行翅帜,如果發(fā)生重排序,另一個(gè)并發(fā)執(zhí)行的線程B就有可能在第4行判斷instance不為null命满。線程B接下來(lái)將訪問(wèn)instance所引用的對(duì)象涝滴,但此時(shí)這個(gè)對(duì)象可能還沒(méi)有被A線程初始化。在知曉問(wèn)題發(fā)生的根源之后胶台,我們可以想出兩個(gè)辦法解決
不允許2和3重排序
允許2和3重排序歼疮,但不允許其他線程“看到”這個(gè)重排序
下面就介紹這兩個(gè)解決方案的具體實(shí)現(xiàn)
基于volatile的解決方案
對(duì)于前面的基于雙重檢查鎖定的方案,只需要做一點(diǎn)小的修改诈唬,就可以實(shí)現(xiàn)線程安全的延遲初始化韩脏。請(qǐng)看下面的示例代碼
publicclassSafeDoubleCheckedLocking{
privatevolatilestaticSafeDoubleCheckedLockinginstance;
privateSafeDoubleCheckedLocking(){
}
publicstaticSafeDoubleCheckedLockinggetInstance(){
if(instance==null){
synchronized(SafeDoubleCheckedLocking.class){
if(instance==null)
instance=newSafeDoubleCheckedLocking();//instance為volatile,現(xiàn)在沒(méi)問(wèn)題了
}
}
returninstance;
}
}
當(dāng)聲明對(duì)象的引用為volatile后铸磅,前面?zhèn)未a談到的2和3之間的重排序赡矢,在多線程環(huán)境中將會(huì)被禁止。
基于類初始化的解決方案
JVM在類的初始化階段(即在Class被加載后阅仔,且被線程使用之前)吹散,會(huì)執(zhí)行類的初始化。在執(zhí)行類的初始化期間八酒,JVM會(huì)去獲取多個(gè)線程對(duì)同一個(gè)類的初始化空民。基于這個(gè)特性羞迷,實(shí)現(xiàn)的示例代碼如下
publicclassInstanceFactory{
privateInstanceFactory(){
}
privatestaticclassInstanceHolder{
publicstaticInstanceFactoryinstance=newInstanceFactory();
}
publicstaticInstanceFactorygetInstance(){
returnInstanceHolder.instance;//這里將導(dǎo)致InstanceHolder類被初始化
}
}
這個(gè)方案的本質(zhì)是允許前面?zhèn)未a談到的2和3重排序界轩,但不允許其他線程“看到”這個(gè)重排序。在InstanceFactory示例代碼中衔瓮,首次執(zhí)行g(shù)etInstance()方法的線程將導(dǎo)致InstanceHolder類被初始化浊猾。由于Java語(yǔ)言是多線程的,多個(gè)線程可能在同一時(shí)間嘗試去初始化同一個(gè)類或接口(比如這里多個(gè)線程可能會(huì)在同一時(shí)刻調(diào)用getInstance()方法來(lái)初始化IInstanceHolder類)热鞍。Java語(yǔ)言規(guī)定与殃,對(duì)于每一個(gè)類和接口C,都有一個(gè)唯一的初始化鎖LC與之對(duì)應(yīng)碍现。從C到LC的映射幅疼,由JVM的具體實(shí)現(xiàn)去自由實(shí)現(xiàn)。JVM在類初始化期間會(huì)獲取這個(gè)初始化鎖昼接,并且每個(gè)線程至少獲取一次鎖來(lái)確保這個(gè)類已經(jīng)被初始化過(guò)了爽篷。
JMM
也許你還存在疑問(wèn),前面談的重排序是什么鬼慢睡?為什么volatile在某方面就能禁止重排序逐工?現(xiàn)在引出本文的另一個(gè)話題JMM(Java Memory Model——Java內(nèi)存模型)铡溪。什么是JMM呢?JMM是一個(gè)抽象概念泪喊,它并不存在棕硫。Java虛擬機(jī)規(guī)范中試圖定義一種Java內(nèi)存模型(JMM)來(lái)屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問(wèn)差異,以實(shí)現(xiàn)讓Java程序在各種平臺(tái)下都能達(dá)到一致的內(nèi)存訪問(wèn)效果袒啼。在此之前哈扮,主流程序語(yǔ)言(如C/C++等)直接使用物理硬件和操作系統(tǒng)的內(nèi)存模型,因此蚓再,會(huì)由于不同平臺(tái)的內(nèi)存模型的差異滑肉,有可能導(dǎo)致程序在一套平臺(tái)上并發(fā)完全正常迫像,而在另一套平臺(tái)上并發(fā)訪問(wèn)卻經(jīng)常出錯(cuò)巡扇,因此在某些場(chǎng)景就必須針對(duì)不同的平臺(tái)來(lái)編寫程序荣倾。
Java線程之間的通信由JMM來(lái)控制卖子,JMM決定一個(gè)線程共享變量的寫入何時(shí)對(duì)另一個(gè)線程可見(jiàn)。JMM保證如果程序是正確同步的切诀,那么程序的執(zhí)行將具有順序一致性养距。從抽象的角度看蝗砾,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量(實(shí)例域矾端、靜態(tài)域和數(shù)據(jù)元素)存儲(chǔ)在主內(nèi)存(Main Memory)中掏击,每個(gè)線程都有一個(gè)私有的本地內(nèi)存(Local Memory),本地內(nèi)存中存儲(chǔ)了該線程以讀/寫共享變量的副本(局部變量须床、方法定義參數(shù)和異常處理參數(shù)是不會(huì)在線程之間共享铐料,它們存儲(chǔ)在線程的本地內(nèi)存中)渐裂。從物理角度上看豺旬,主內(nèi)存僅僅是虛擬機(jī)內(nèi)存的一部分,與物理硬件的主內(nèi)存名字一樣柒凉,兩者可以互相類比族阅;而本地內(nèi)存,可與處理器高速緩存類比膝捞。Java內(nèi)存模型的抽象示意圖如圖所示
這里先介紹幾個(gè)基礎(chǔ)概念:8種操作指令坦刀、內(nèi)存屏障、順序一致性模型蔬咬、as-if-serial鲤遥、happens-before 、數(shù)據(jù)依賴性林艘、 重排序盖奈。
8種操作指令
關(guān)于主內(nèi)存與本地內(nèi)存之間具體的交互協(xié)議,即一個(gè)變量如何從主內(nèi)存拷貝到本地內(nèi)存狐援、如何從本地內(nèi)存同步回主內(nèi)存之類的實(shí)現(xiàn)細(xì)節(jié)钢坦,JMM中定義了以下8種操作來(lái)完成究孕,虛擬機(jī)實(shí)現(xiàn)時(shí)必須保證下面提及的每種操作都是原子的、不可再分的(對(duì)于double和long類型的遍歷來(lái)說(shuō)爹凹,load厨诸、store、read和write操作在某些平臺(tái)上允許有例外):
lock(鎖定):作用于主內(nèi)存的變量禾酱,它把一個(gè)變量標(biāo)識(shí)為一條線程獨(dú)立的狀態(tài)微酬。
unlock(解鎖):作用于主內(nèi)存的變量,它把一個(gè)處于鎖定狀態(tài)的變量釋放出來(lái)宇植,釋放后的變量才可以被其他線程鎖定得封。
read(讀取):作用于主內(nèi)存的變量,它把一個(gè)變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的本地內(nèi)存中指郁,以便隨后的load動(dòng)作使用忙上。
load(載入):作用于本地內(nèi)存的變量,它把read操作從主內(nèi)存中得到變量值放入本地內(nèi)存的變量副本中闲坎。
use(使用):作用于本地內(nèi)存的變量疫粥,它把本地內(nèi)存中一個(gè)變量的值傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用到變量的值的字節(jié)碼指令時(shí)將會(huì)執(zhí)行這個(gè)操作腰懂。
assign(賦值):作用于本地內(nèi)存的變量梗逮,它把一個(gè)從執(zhí)行引擎接收到的值賦給本地內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼指令時(shí)執(zhí)行這個(gè)操作绣溜。
store(存儲(chǔ)):作用于本地內(nèi)存的變量慷彤,它把本地內(nèi)存中的一個(gè)變量的值傳送到主內(nèi)存中,以便隨后的write操作使用怖喻。
write(寫入):作用于主內(nèi)存的變量底哗,它把store操作從本地內(nèi)存中提到的變量的值放入到主內(nèi)存的變量中。
如果要把一個(gè)變量從主內(nèi)存模型復(fù)制到本地內(nèi)存锚沸,那就要順序的執(zhí)行read和load操作跋选,如果要把變量從本地內(nèi)存同步回主內(nèi)存,就要順序的執(zhí)行store和write操作哗蜈。注意前标,Java內(nèi)存模型只要求上述兩個(gè)操作必須按順序執(zhí)行,而沒(méi)有保證是連續(xù)執(zhí)行距潘。也就是說(shuō)read與load之間炼列、store與write之間是可插入其他指令的,如對(duì)主內(nèi)存中的變量a音比、b進(jìn)行訪問(wèn)時(shí)俭尖,一種可能出現(xiàn)的順序是read a read b、load b硅确、load a目溉。
內(nèi)存屏障
內(nèi)存屏障是一組處理器指令(前面的8個(gè)操作指令)明肮,用于實(shí)現(xiàn)對(duì)內(nèi)存操作的順序限制。包括LoadLoad, LoadStore, StoreLoad, StoreStore共4種內(nèi)存屏障缭付。內(nèi)存屏障存在的意義是什么呢柿估?它是在Java編譯器生成指令序列的適當(dāng)位置插入內(nèi)存屏障指令來(lái)禁止特定類型的處理器重排序,從而讓程序按我們預(yù)想的流程去執(zhí)行陷猫,內(nèi)存屏障是與相應(yīng)的內(nèi)存重排序相對(duì)應(yīng)的秫舌。JMM把內(nèi)存屏障指令分為4類
StoreLoad Barriers是一個(gè)“全能型 ”的屏障,它同時(shí)具有其他3個(gè)屏障的效果⌒迕剩現(xiàn)在的多數(shù)處理器大多支持該屏障(其他類型的屏障不一定被所有處理器支持)足陨。執(zhí)行該屏障開(kāi)銷會(huì)很昂貴,因?yàn)楫?dāng)前處理器通常要把寫緩沖區(qū)中的數(shù)據(jù)全部刷新到內(nèi)存中娇未。
數(shù)據(jù)依賴性
如果兩個(gè)操作訪問(wèn)同一個(gè)變量墨缘,且這兩個(gè)操作中有一個(gè)為寫操作,此時(shí)這兩個(gè)操作之間就存在數(shù)據(jù)依賴性零抬。數(shù)據(jù)依賴性分3種類型:寫后讀镊讼、寫后寫、讀后寫平夜。這3種情況蝶棋,只要重排序兩個(gè)操作的執(zhí)行順序,程序的執(zhí)行結(jié)果就會(huì)被改變忽妒。編譯器和處理器可能對(duì)操作進(jìn)行重排序玩裙。而它們進(jìn)行重排序時(shí),會(huì)遵守?cái)?shù)據(jù)依賴性段直,不會(huì)改變數(shù)據(jù)依賴關(guān)系的兩個(gè)操作的執(zhí)行順序吃溅。
這里所說(shuō)的數(shù)據(jù)依賴性僅針對(duì)單個(gè)處理器中執(zhí)行的指令序列和單個(gè)線程中執(zhí)行的操作,不同處理器之間和不同線程之間的數(shù)據(jù)依賴性不被編譯器和處理器考慮坷牛。
順序一致性內(nèi)存模型
順序一致性內(nèi)存模型是一個(gè)理論參考模型罕偎,在設(shè)計(jì)的時(shí)候很澄,處理器的內(nèi)存模型和編程語(yǔ)言的內(nèi)存模型都會(huì)以順序一致性內(nèi)存模型作為參照京闰。它有兩個(gè)特性:
一個(gè)線程中的所有操作必須按照程序的順序來(lái)執(zhí)行
(不管程序是否同步)所有線程都只能看到一個(gè)單一的操作執(zhí)行順序。在順序一致性的內(nèi)存模型中甩苛,每個(gè)操作必須原子執(zhí)行并且立刻對(duì)所有線程可見(jiàn)蹂楣。
從順序一致性模型中,我們可以知道程序所有操作完全按照程序的順序串行執(zhí)行讯蒲。而在JMM中痊土,臨界區(qū)內(nèi)的代碼可以重排序(但JMM不允許臨界區(qū)內(nèi)的代碼“逸出”到臨界區(qū)外,那樣就破壞監(jiān)視器的語(yǔ)義)墨林。JMM會(huì)在退出臨界區(qū)和進(jìn)入臨界區(qū)這兩個(gè)關(guān)鍵時(shí)間點(diǎn)做一些特別處理赁酝,使得線程在這兩個(gè)時(shí)間點(diǎn)具有與順序一致性模型相同的內(nèi)存視圖犯祠。雖然線程A在臨界區(qū)內(nèi)做了重排序,但由于監(jiān)視器互斥執(zhí)行的特性酌呆,這里的線程B根本無(wú)法“觀察”到線程A在臨界區(qū)內(nèi)的重排序衡载。這種重排序既提高了執(zhí)行效率,又沒(méi)有改變程序的執(zhí)行結(jié)果隙袁。像前面單例示例的類初始化解決方案就是采用了這個(gè)思想痰娱。
as-if-serial
as-if-serial的意思是不管怎么重排序,(單線程)程序的執(zhí)行結(jié)果不能改變菩收。編譯器梨睁、runtime和處理器都必須遵守as-if-serial語(yǔ)義。為了遵守as-if-serial語(yǔ)義娜饵,編譯器和處理器不會(huì)對(duì)存在數(shù)據(jù)依賴關(guān)系的操作做重排序坡贺。
as-if-serial語(yǔ)義把單線程程序保護(hù)了起來(lái),遵守as-if-serial語(yǔ)義的編譯器箱舞、runtime和處理器共同為編寫單線程程序的程序員創(chuàng)建了一個(gè)幻覺(jué):?jiǎn)尉€程程序是按程序的順序來(lái)執(zhí)行的拴念。as-if-serial語(yǔ)義使單線程程序員無(wú)需擔(dān)心重排序會(huì)干擾他們,也無(wú)需擔(dān)心內(nèi)存可見(jiàn)性問(wèn)題褐缠。
happens-before
happens-before是JMM最核心的概念政鼠。從JDK5開(kāi)始,Java使用新的JSR-133內(nèi)存模型队魏,JSR-133?使用happens-before的概念闡述操作之間的內(nèi)存可見(jiàn)性公般,如果一個(gè)操作執(zhí)行的結(jié)果需要對(duì)另一個(gè)操作可見(jiàn),那么這兩個(gè)操作之間必須存在happens-before關(guān)系胡桨。
happens-before規(guī)則如下:
程序次序法則:線程中的每個(gè)動(dòng)作 A 都 happens-before 于該線程中的每一個(gè)動(dòng)作 B官帘,其中,在程序中昧谊,所有的動(dòng)作 B 都出現(xiàn)在動(dòng)作 A 之后刽虹。(注:此法則只是要求遵循 as-if-serial語(yǔ)義)
監(jiān)視器鎖法則:對(duì)一個(gè)監(jiān)視器鎖的解鎖 happens-before 于每一個(gè)后續(xù)對(duì)同一監(jiān)視器鎖的加鎖。(顯式鎖的加鎖和解鎖有著與內(nèi)置鎖呢诬,即監(jiān)視器鎖相同的存儲(chǔ)語(yǔ)意涌哲。)
volatile變量法則:對(duì) volatile 域的寫入操作 happens-before 于每一個(gè)后續(xù)對(duì)同一域的讀操作。(原子變量的讀寫操作有著與 volatile 變量相同的語(yǔ)意尚镰。)(volatile變量具有可見(jiàn)性和讀寫原子性阀圾。)
線程啟動(dòng)法則:在一個(gè)線程里,對(duì) Thread.start 的調(diào)用會(huì) happens-before 于每一個(gè)啟動(dòng)線程中的動(dòng)作狗唉。
線程終止法則:線程中的任何動(dòng)作都 happens-before 于其他線程檢測(cè)到這個(gè)線程已終結(jié)初烘,或者從 Thread.join 方法調(diào)用中成功返回,或者 Thread.isAlive 方法返回false。
中斷法則法則:一個(gè)線程調(diào)用另一個(gè)線程的 interrupt 方法 happens-before 于被中斷線程發(fā)現(xiàn)中斷(通過(guò)拋出InterruptedException肾筐, 或者調(diào)用 isInterrupted 方法和 interrupted 方法)哆料。
終結(jié)法則:一個(gè)對(duì)象的構(gòu)造函數(shù)的結(jié)束 happens-before 于這個(gè)對(duì)象 finalizer 開(kāi)始。
傳遞性:如果 A happens-before 于 B吗铐,且 B happens-before 于 C剧劝,則 A happens-before 于 C。
happens-before與JMM的關(guān)系如下圖所示
as-if-serial語(yǔ)義和happens-before本質(zhì)上一樣抓歼,參考順序一致性內(nèi)存模型的理論讥此,在不改變程序執(zhí)行結(jié)果的前提下,給編譯器和處理器以最大的自由度谣妻,提高并行度萄喳。
重排序
終于談到我們反復(fù)提及的重排序了,重排序是指編譯器和處理器為了優(yōu)化程序性能而對(duì)指令序列進(jìn)行重新排序的一種手段蹋半。重排序分3種類型他巨。
編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語(yǔ)義(as-if-serial )的前提下减江,可以重新安排語(yǔ)句的執(zhí)行順序染突。
指令級(jí)并行的重排序。現(xiàn)代處理器采用了指令級(jí)并行技術(shù)(Instruction Level Parallelism辈灼,ILP)來(lái)將多條指令重疊執(zhí)行份企。如果不存在數(shù)據(jù)依賴性,處理器可以改變語(yǔ)句對(duì)機(jī)器指令的執(zhí)行順序巡莹。
內(nèi)存系統(tǒng)的重排序司志。由于處理器使用緩存和讀/寫緩沖區(qū),這使得加載和存儲(chǔ)操作看上去可能是在亂序執(zhí)行降宅。
從Java源代碼到最終實(shí)際執(zhí)行的指令序列骂远,會(huì)分別經(jīng)歷下面3種重排序
上述的1屬于編譯器重排序,2和3屬于處理器重排序腰根。這些重排序可能會(huì)導(dǎo)致多線程程序出現(xiàn)內(nèi)存可見(jiàn)性問(wèn)題激才。對(duì)于編譯器,JMM的編譯器重排序規(guī)則會(huì)禁止特定類型的編譯器重排序(不是所有的編譯器重排序都要禁止)额嘿。對(duì)于處理器重排序瘸恼,JMM的處理器重排序規(guī)則會(huì)要求Java編譯器在生成指令序列時(shí),插入特定類型的內(nèi)存屏障指令岩睁,通過(guò)內(nèi)存屏障指令來(lái)禁止特定類型的處理器重排序钞脂。
JMM屬于語(yǔ)言級(jí)的內(nèi)存模型揣云,它確保在不同的編譯器和不同的處理器平臺(tái)之上捕儒,通過(guò)禁止特定類型的編譯器重排序和處理器重排序,為程序員提供一致的內(nèi)存可見(jiàn)性保證。
從JMM設(shè)計(jì)者的角度來(lái)說(shuō)刘莹,在設(shè)計(jì)JMM時(shí)阎毅,需要考慮兩個(gè)關(guān)鍵因素:
程序員對(duì)內(nèi)存模型的使用。程序員希望內(nèi)存模型易于理解点弯,易于編程扇调。程序員希望基于一個(gè)強(qiáng)內(nèi)存模型(程序盡可能的順序執(zhí)行)來(lái)編寫代碼。
編譯器和處理器對(duì)內(nèi)存模型的實(shí)現(xiàn)抢肛。編譯器和處理器希望內(nèi)存模型對(duì)它們的束縛越少越好狼钮,這樣它們就可以做盡可能多的優(yōu)化(對(duì)程序重排序,做盡可能多的并發(fā))來(lái)提高性能捡絮。編譯器和處理器希望實(shí)現(xiàn)一個(gè)弱內(nèi)存模型熬芜。
JMM設(shè)計(jì)就需要在這兩者之間作出協(xié)調(diào)。JMM對(duì)程序采取了不同的策略:
對(duì)于會(huì)改變程序執(zhí)行結(jié)果的重排序福稳,JMM要求編譯器和處理器必須禁止這種重排序涎拉。
對(duì)于不會(huì)改變程序執(zhí)行結(jié)果的重排序,JMM對(duì)編譯器和處理器不作要求(JMM允許這種重排序)的圆。
介紹完了這幾個(gè)基本概念鼓拧,我們不難推斷出JMM是圍繞著在并發(fā)過(guò)程中如何處理原子性、可見(jiàn)性和有序性這三個(gè)特征來(lái)建立的越妈。
原子性:由Java內(nèi)存模型來(lái)直接保證的原子性操作就是我們前面介紹的8個(gè)原子操作指令季俩,其中l(wèi)ock(lock指令實(shí)際在處理器上原子操作體現(xiàn)對(duì)總線加鎖或?qū)彺婕渔i)和unlock指令操作JVM并未直接開(kāi)放給用戶使用,但是卻提供了更高層次的字節(jié)碼指令monitorenter和monitorexit來(lái)隱式使用這兩個(gè)操作梅掠,這兩個(gè)字節(jié)碼指令反映到Java代碼中就是同步塊——synchronize關(guān)鍵字种玛,因此在synchronized塊之間的操作也具備原子性。除了synchronize瓤檐,在Java中另一個(gè)實(shí)現(xiàn)原子操作的重要方式是自旋CAS赂韵,它是利用處理器提供的cmpxchg指令實(shí)現(xiàn)的。至于自旋CAS后面J.U.C中會(huì)詳細(xì)介紹挠蛉,它和volatile是整個(gè)J.U.C底層實(shí)現(xiàn)的核心祭示。
可見(jiàn)性:可見(jiàn)性是指一個(gè)線程修改了共享變量的值,其他線程能夠立即得知這個(gè)修改谴古。而我們上文談的happens-before原則禁止某些處理器和編譯器的重排序质涛,來(lái)保證了JMM的可見(jiàn)性。而體現(xiàn)在程序上掰担,實(shí)現(xiàn)可見(jiàn)性的關(guān)鍵字包含了volatile汇陆、synchronize和final。
有序性:談到有序性就涉及到前面說(shuō)的重排序和順序一致性內(nèi)存模型带饱。我們也都知道了as-if-serial是針對(duì)單線程程序有序的毡代,即使存在重排序阅羹,但是最終程序結(jié)果還是不變的,而多線程程序的有序性則體現(xiàn)在JMM通過(guò)插入內(nèi)存屏障指令教寂,禁止了特定類型處理器的重排序捏鱼。通過(guò)前面8個(gè)操作指令和happens-before原則介紹,也不難推斷出酪耕,volatile和synchronized兩個(gè)關(guān)鍵字來(lái)保證線程之間的有序性导梆,volatile本身就包含了禁止指令重排序的語(yǔ)義,而synchronized則是由監(jiān)視器法則獲得迂烁。
J.U.C
談完了JMM看尼,那么Java相關(guān)類庫(kù)是如何實(shí)現(xiàn)的呢?這里就談?wù)凧.U.C( java.util.concurrent)盟步,先來(lái)張J.U.C的思維導(dǎo)圖
不難看出狡忙,J.U.C由atomic、locks址芯、tools灾茁、collections、executor這五部分組成谷炸。它們的實(shí)現(xiàn)基于volatile的讀寫和CAS所具有的volatile讀和寫北专。AQS(AbstractQueuedSynchronizer,隊(duì)列同步器)旬陡、非阻塞數(shù)據(jù)結(jié)構(gòu)和原子變量類拓颓,這些J.U.C中的基礎(chǔ)類都是使用了這種模式實(shí)現(xiàn)的,而J.U.C中的高層類又依賴于這些基礎(chǔ)類來(lái)實(shí)現(xiàn)的描孟。從整體上看驶睦,J.U.C的實(shí)現(xiàn)示意圖如下
也許你對(duì)volatile和CAS的底層實(shí)現(xiàn)原理不是很了解,這里先這里先簡(jiǎn)單介紹下它們的底層實(shí)現(xiàn)
volatile
Java語(yǔ)言規(guī)范第三版對(duì)volatile的定義為:Java編程語(yǔ)言允許線程訪問(wèn)共享變量匿醒,為了確保共享變量能被準(zhǔn)確和一致性的更新场航,線程應(yīng)該確保通過(guò)排他鎖單獨(dú)獲得這個(gè)變量。如果一個(gè)字段被聲明為volatile廉羔,Java內(nèi)存模型確保這個(gè)所有線程看到這個(gè)值的變量是一致的溉痢。而volatile是如何來(lái)保證可見(jiàn)性的呢?如果對(duì)聲明了volatile的變量進(jìn)行寫操作憋他,JVM就會(huì)向處理器發(fā)送一條Lock前綴的指令孩饼,將這個(gè)變量所在緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存(Lock指令會(huì)在聲言該信號(hào)期間鎖總線/緩存,這樣就獨(dú)占了系統(tǒng)內(nèi)存)竹挡。但是镀娶,就算是寫回到內(nèi)存,如果其他處理器緩存的值還是舊的揪罕,再執(zhí)行計(jì)算操作就會(huì)有問(wèn)題梯码。所以宝泵,在多處理器下,為了保證各個(gè)處理器的緩存是一致的忍些,就會(huì)實(shí)現(xiàn)緩存一致性協(xié)議鲁猩,每個(gè)處理器通過(guò)嗅探在總線(注意處理器不直接跟系統(tǒng)內(nèi)存交互坎怪,而是通過(guò)總線)上傳播的數(shù)據(jù)來(lái)檢查自己緩存的值是不是過(guò)期了罢坝,當(dāng)處理器發(fā)現(xiàn)直接緩存行對(duì)應(yīng)的內(nèi)存地址被修改,就會(huì)將當(dāng)前處理器的緩存行設(shè)置成無(wú)效狀態(tài)搅窿,當(dāng)處理器對(duì)這個(gè)數(shù)據(jù)進(jìn)行修改操作的時(shí)候嘁酿,會(huì)重新從系統(tǒng)內(nèi)存中把數(shù)據(jù)讀到處理器緩存里。
CAS
CAS其實(shí)應(yīng)用挺廣泛的男应,我們常常聽(tīng)到的悲觀鎖樂(lè)觀鎖的概念闹司,樂(lè)觀鎖(無(wú)鎖)指的就是CAS。這里只是簡(jiǎn)單說(shuō)下在并發(fā)的應(yīng)用沐飘,所謂的樂(lè)觀并發(fā)策略游桩,通俗的說(shuō),就是先進(jìn)性操作耐朴,如果沒(méi)有其他線程爭(zhēng)用共享數(shù)據(jù)借卧,那操作就成功了,如果共享數(shù)據(jù)有爭(zhēng)用筛峭,產(chǎn)生了沖突铐刘,那就采取其他的補(bǔ)償措施(最常見(jiàn)的補(bǔ)償措施就是不斷重試,治到成功為止影晓,這里其實(shí)也就是自旋CAS的概念)镰吵,這種樂(lè)觀的并發(fā)策略的許多實(shí)現(xiàn)都不需要把線程掛起,因此這種操作也被稱為非阻塞同步挂签。而CAS這種樂(lè)觀并發(fā)策略操作和沖突檢測(cè)這兩個(gè)步驟具備的原子性疤祭,是靠什么保證的呢?硬件饵婆,硬件保證了一個(gè)從語(yǔ)義上看起來(lái)需要多次操作的行為只通過(guò)一條處理器指令就能完成画株。
也許你會(huì)存在疑問(wèn),為什么這種無(wú)鎖的方案一般會(huì)比直接加鎖效率更高呢啦辐?這里其實(shí)涉及到線程的實(shí)現(xiàn)和線程的狀態(tài)轉(zhuǎn)換谓传。實(shí)現(xiàn)線程主要有三種方式:使用內(nèi)核線程實(shí)現(xiàn)、使用用戶線程實(shí)現(xiàn)和使用用戶線程加輕量級(jí)進(jìn)程混合實(shí)現(xiàn)芹关。而Java的線程實(shí)現(xiàn)則依賴于平臺(tái)使用的線程模型续挟。至于狀態(tài)轉(zhuǎn)換,Java定義了6種線程狀態(tài)侥衬,在任意一個(gè)時(shí)間點(diǎn)诗祸,一個(gè)線程只能有且只有其中的一種狀態(tài)跑芳,這6種狀態(tài)分別是:新建、運(yùn)行直颅、無(wú)限期等待博个、限期等待、阻塞功偿、結(jié)束盆佣。 Java的線程是映射到操作系統(tǒng)的原生線程之上的,如果要阻塞或喚醒一個(gè)線程械荷,都需要操作系統(tǒng)來(lái)幫忙完成共耍,這就需要從用戶態(tài)轉(zhuǎn)換到核心態(tài)中,因此狀態(tài)轉(zhuǎn)換需要耗費(fèi)很多的處理器時(shí)間吨瞎。對(duì)于簡(jiǎn)單的同步塊(被synchronized修飾的方法)痹兜,狀態(tài)轉(zhuǎn)換消耗的時(shí)間可能比用戶代碼執(zhí)行的時(shí)間還要長(zhǎng)。所以出現(xiàn)了這種優(yōu)化方案颤诀,在操作系統(tǒng)阻塞線程之間引入一段自旋過(guò)程或一直自旋直到成功為止字旭。避免頻繁的切入到核心態(tài)之中。
但是這種方案其實(shí)也并不完美崖叫,在這里就說(shuō)下CAS實(shí)現(xiàn)原子操作的三大問(wèn)題
ABA問(wèn)題遗淳。因?yàn)镃AS需要在操作值的時(shí)候,檢查值有沒(méi)有變化归露,如果沒(méi)有發(fā)生變化則更新洲脂,但是如果一個(gè)值原來(lái)是A,變成了B剧包,又變成了A恐锦,那么使用CAS進(jìn)行檢查時(shí)會(huì)發(fā)現(xiàn)它的值沒(méi)有變化,但是實(shí)際上發(fā)生變化了疆液。ABA解決的思路是使用版本號(hào)一铅。在變量前面追加上版本號(hào),每次變量更新的時(shí)候把版本號(hào)加1堕油。JDK的Atomic包里提供了一個(gè)類AtomicStampedReference來(lái)解決ABA問(wèn)題潘飘。不過(guò)目前來(lái)說(shuō)這個(gè)類比較“雞肋”,大部分情況下ABA問(wèn)題不會(huì)影響程序并發(fā)的正確性掉缺,如果需要解決ABA問(wèn)題卜录,改用原來(lái)的互斥同步可能會(huì)比原子類更高效。
循環(huán)時(shí)間長(zhǎng)開(kāi)銷大眶明。自旋CAS如果長(zhǎng)時(shí)間不成功艰毒,會(huì)給CPU帶來(lái)非常大的執(zhí)行開(kāi)銷。所以說(shuō)如果是長(zhǎng)時(shí)間占用鎖執(zhí)行的程序搜囱,這種方案并不適用于此丑瞧。
只能保證一個(gè)共享變量的原子操作柑土。當(dāng)對(duì)一個(gè)共享變量執(zhí)行操作時(shí),我們可以使用自旋CAS來(lái)保證原子性绊汹,但是對(duì)多個(gè)共享變量的操作時(shí)稽屏,自旋CAS就無(wú)法保證操作的原子性,這個(gè)時(shí)候可以用鎖西乖。狐榔、
談完了這兩個(gè)概念,下面我們就來(lái)逐個(gè)分析這五部分的具體源碼實(shí)現(xiàn)
atomic
atomic包的原子操作類提供了一種簡(jiǎn)單浴栽、性能高效荒叼、線程安全操作一個(gè)變量的方式轿偎。atomic包里一共13個(gè)類典鸡,屬于4種類型的原子更新方式,分別是原子更新基本類型坏晦、原子更新數(shù)組萝玷、原子更新引用、原子更新屬性昆婿。atomic包里的類基本使用Unsafe實(shí)現(xiàn)的包裝類球碉。
下面通過(guò)一個(gè)簡(jiǎn)單的CAS方式實(shí)現(xiàn)計(jì)數(shù)器(一個(gè)線程安全的計(jì)數(shù)器方法safeCount和一個(gè)非線程安全的計(jì)數(shù)器方法count)的示例來(lái)說(shuō)下
publicclassCASTest{
publicstaticvoidmain(String[]args){
finalCountercas=newCounter();
Listts=newArrayList(600);
longstart=System.currentTimeMillis();
for(intj=0;j<100;j++){
Threadt=newThread(newRunnable(){
@Override
publicvoidrun(){
for(inti=0;i<10000;i++){
cas.count();
cas.safeCount();
}
}
});
ts.add(t);
}
for(Threadt:ts){
t.start();
}
for(Threadt:ts){
try{
t.join();
}catch(InterruptedExceptione){
e.printStackTrace();
}
}
System.out.println(cas.i);
System.out.println(cas.atomicI.get());
System.out.println(System.currentTimeMillis()-start);
}
}
publicclassCounter{
publicAtomicIntegeratomicI=newAtomicInteger(0);
publicinti=0;
/**
* 使用CAS實(shí)現(xiàn)線程安全計(jì)數(shù)器
*/
publicvoidsafeCount(){
for(;;){
inti=atomicI.get();
booleansuc=atomicI.compareAndSet(i,++i);
if(suc){
break;
}
}
}
/**
* 非線程安全計(jì)數(shù)器
*/
publicvoidcount(){
i++;
}
}
safeCount()方法的代碼塊其實(shí)是getandIncrement()方法的實(shí)現(xiàn),源碼for循環(huán)體第一步優(yōu)先取得atomicI里存儲(chǔ)的數(shù)值仓蛆,第二步對(duì)atomicI的當(dāng)前數(shù)值進(jìn)行加1操作睁冬,關(guān)鍵的第三步調(diào)用compareAndSet()方法來(lái)進(jìn)行原子更新操作,該方法先檢查當(dāng)前數(shù)值是否等于current看疙,等于意味著atomicI的值沒(méi)有被其他線程修改過(guò)豆拨,則將atomicI的當(dāng)前數(shù)值更新成next的值,如果不等compareAndSet()方法會(huì)返回false能庆,程序則進(jìn)入for循環(huán)重新進(jìn)行compareAndSet()方法操作進(jìn)行不斷嘗試直到成功為止施禾。在這里我們跟蹤下compareAndSet()方法如下
publicfinalbooleancompareAndSet(intexpect,intupdate){
returnunsafe.compareAndSwapInt(this,valueOffset,expect,update);
}
從上面源碼我們發(fā)現(xiàn)是使用Unsafe實(shí)現(xiàn)的,其實(shí)atomic里的類基本都是使用Unsafe實(shí)現(xiàn)的搁胆。我們?cè)倩氐竭@個(gè)本地方法調(diào)用弥搞,這個(gè)本地方法在openjdk中依次調(diào)用c++代碼為unsafe.cpp、atomic.app和atomic_windows_x86.inline.hpp渠旁。關(guān)于本地方法實(shí)現(xiàn)的源代碼這里就不貼出來(lái)了攀例,其實(shí)大體上是程序會(huì)根據(jù)當(dāng)前處理器的類型來(lái)決定是否為cmpxchg指令添加lock前綴。如果程序是在多處理器上運(yùn)行顾腊,就為cmpxchg指令加上lock前綴(Lock Cmpxchg)粤铭。反之,如果程序是在單處理器上運(yùn)行投慈,就省略lock前綴(單處理器自身就會(huì)維護(hù)單處理器內(nèi)的順序一致性承耿,不需要lock前綴提供的內(nèi)存屏障效果)冠骄。
locks
鎖是用來(lái)控制多個(gè)線程訪問(wèn)共享資源的形式,Java SE 5之后加袋,J.U.C中新增了locks來(lái)實(shí)現(xiàn)鎖功能凛辣,它提供了與synchronized關(guān)鍵字類似的同步功能。只是在使用時(shí)需要顯示的獲取和釋放鎖职烧。雖然它缺少了隱式獲取和釋放鎖的便捷性妻味,但是卻擁有了鎖獲取和釋放的可操作性、可中斷的獲取鎖及超時(shí)獲取鎖等多種synchronized關(guān)鍵字不具備的同步特性乾蛤。
locks在這我們只介紹下核心的AQS(AbstractQueuedSynchronizer翅睛,隊(duì)列同步器),AQS是用來(lái)構(gòu)建鎖或者其他同步組件的基礎(chǔ)框架足删,它使用一個(gè)用volatile修飾的int成員變量表示同步狀態(tài)寿谴。通過(guò)內(nèi)置的FIFO隊(duì)列來(lái)完成資源獲取線程的排隊(duì)工作。同步器的主要使用方式是繼承失受,子類通過(guò)繼承同步器并實(shí)現(xiàn)它的抽象方法來(lái)管理同步狀態(tài)讶泰,在抽象方法的實(shí)現(xiàn)過(guò)程免不了要對(duì)同步狀態(tài)進(jìn)行更改,這時(shí)候就會(huì)使用到AQS提供的3個(gè)方法:getState()拂到、setState()和compareAndSetState()來(lái)進(jìn)行操作痪署,這是因?yàn)樗鼈兡軌虮WC狀態(tài)的改變是原子性的。為什么這么設(shè)計(jì)呢兄旬?因?yàn)殒i是面向使用者的狼犯,它定義了使用者與鎖交互的接口,隱藏了實(shí)現(xiàn)細(xì)節(jié)领铐,而AQS面向的是鎖的實(shí)現(xiàn)者悯森,它簡(jiǎn)化了鎖的實(shí)現(xiàn)方式,屏蔽了同步狀態(tài)管理罐孝、線程的排隊(duì)呐馆、等待與喚醒等底層操作。鎖和AQS很好的隔離了使用者和實(shí)現(xiàn)者鎖關(guān)注的領(lǐng)域莲兢。
現(xiàn)在我們就自定義一個(gè)獨(dú)占鎖來(lái)詳細(xì)解釋下AQS的實(shí)現(xiàn)機(jī)制
publicclassMuteximplementsLock{
privatestaticclassSyncextendsAbstractQueuedSynchronizer{
privatestaticfinallongserialVersionUID=-4387327721959839431L;
protectedbooleanisHeldExclusively(){
returngetState()==1;
}
publicbooleantryAcquire(intacquires){
assertacquires==1;// Otherwise unused
if(compareAndSetState(0,1)){
setExclusiveOwnerThread(Thread.currentThread());
returntrue;
}
returnfalse;
}
protectedbooleantryRelease(intreleases){
assertreleases==1;// Otherwise unused
if(getState()==0)
thrownewIllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
returntrue;
}
ConditionnewCondition(){
returnnewConditionObject();
}
}
privatefinalSyncsync=newSync();
publicvoidlock(){
sync.acquire(1);
}
publicbooleantryLock(){
returnsync.tryAcquire(1);
}
publicvoidunlock(){
sync.release(1);
}
publicConditionnewCondition(){
returnsync.newCondition();
}
publicbooleanisLocked(){
returnsync.isHeldExclusively();
}
publicbooleanhasQueuedThreads(){
returnsync.hasQueuedThreads();
}
publicvoidlockInterruptibly()throwsInterruptedException{
sync.acquireInterruptibly(1);
}
publicbooleantryLock(longtimeout,TimeUnitunit)throwsInterruptedException{
returnsync.tryAcquireNanos(1,unit.toNanos(timeout));
}
}
實(shí)現(xiàn)自定義組件的時(shí)候汹来,我們可以看到,AQS可重寫的方法是tryAcquire()——獨(dú)占式獲取同步狀態(tài)改艇、tryRelease()——獨(dú)占式釋放同步狀態(tài)收班、tryAcquireShared()——共享式獲取同步狀態(tài)、tryReleaseShared ()——共享式釋放同步狀態(tài)谒兄、isHeldExclusively()——是否被當(dāng)前線程所獨(dú)占摔桦。這個(gè)示例中,獨(dú)占鎖Mutex是一個(gè)自定義同步組件,它在同一時(shí)刻只允許一個(gè)線程占有鎖邻耕。Mutex中定義了一個(gè)靜態(tài)內(nèi)部類鸥咖,該內(nèi)部類繼承了同步器并實(shí)現(xiàn)了獨(dú)占式獲取和釋放同步狀態(tài)。在tryAcquire()中兄世,如果經(jīng)過(guò)CAS設(shè)置成功(同步狀態(tài)設(shè)置為1)啼辣,則表示獲取了同步狀態(tài),而在tryRelease()中御滩,只是將同步狀態(tài)重置為0鸥拧。接著我們對(duì)比一下重入鎖(ReentrantLock)的源碼實(shí)現(xiàn)
publicclassReentrantLockimplementsLock,java.io.Serializable{
privatestaticfinallongserialVersionUID=7373984872572414699L;
/** Synchronizer providing all implementation mechanics */
privatefinalSyncsync;
/**
* Base of synchronization control for this lock. Subclassed
* into fair and nonfair versions below. Uses AQS state to
* represent the number of holds on the lock.
*/
abstractstaticclassSyncextendsAbstractQueuedSynchronizer{
privatestaticfinallongserialVersionUID=-5179523762034025860L;
/**
* Performs {@link Lock#lock}. The main reason for subclassing
* is to allow fast path for nonfair version.
*/
abstractvoidlock();
/**
* Performs non-fair tryLock.? tryAcquire is
* implemented in subclasses, but both need nonfair
* try for trylock method.
*/
finalbooleannonfairTryAcquire(intacquires){
finalThreadcurrent=Thread.currentThread();
intc=getState();
if(c==0){
if(compareAndSetState(0,acquires)){
setExclusiveOwnerThread(current);
returntrue;
}
}
elseif(current==getExclusiveOwnerThread()){
intnextc=c+acquires;
if(nextc<0)// overflow
thrownewError("Maximum lock count exceeded");
setState(nextc);
returntrue;
}
returnfalse;
}
protectedfinalbooleantryRelease(intreleases){
intc=getState()-releases;
if(Thread.currentThread()!=getExclusiveOwnerThread())
thrownewIllegalMonitorStateException();
booleanfree=false;
if(c==0){
free=true;
setExclusiveOwnerThread(null);
}
setState(c);
returnfree;
}
protectedfinalbooleanisHeldExclusively(){
// While we must in general read state before owner,
// we don't need to do so to check if current thread is owner
returngetExclusiveOwnerThread()==Thread.currentThread();
}
finalConditionObjectnewCondition(){
returnnewConditionObject();
}
// Methods relayed from outer class
finalThreadgetOwner(){
returngetState()==0?null:getExclusiveOwnerThread();
}
finalintgetHoldCount(){
returnisHeldExclusively()?getState():0;
}
finalbooleanisLocked(){
returngetState()!=0;
}
/**
* Reconstitutes this lock instance from a stream.
* @param s the stream
*/
privatevoidreadObject(java.io.ObjectInputStreams)
throwsjava.io.IOException,ClassNotFoundException{
s.defaultReadObject();
setState(0);// reset to unlocked state
}
}
/**
* Sync object for non-fair locks
*/
staticfinalclassNonfairSyncextendsSync{
//todo sth...
}
//todo sth...
}
重入鎖分公平鎖和不公平鎖,默認(rèn)使用的是不公平鎖削解,在這我們看到實(shí)現(xiàn)重入鎖大體上跟我們剛才自定義的獨(dú)占鎖差不多富弦,但是有什么區(qū)別呢?我們看看重入鎖nonfairTryAcquire()方法實(shí)現(xiàn):首先獲取同步狀態(tài)(默認(rèn)是0)氛驮,如果是0的話腕柜,CAS設(shè)置同步狀態(tài),非0的話則判斷當(dāng)前線程是否已占有鎖柳爽,如果是的話媳握,則偏向更新同步狀態(tài)碱屁。從這里我們不難推斷出重入鎖的概念磷脯,同一個(gè)線程可以多次獲得同一把鎖,在釋放的時(shí)候也必須釋放相同次數(shù)的鎖娩脾。通過(guò)對(duì)比相信大家對(duì)自定義一個(gè)鎖有了一個(gè)初步的概念赵誓,也許你存在疑問(wèn)我們重寫的這幾個(gè)方法在AQS哪地方用呢?現(xiàn)在我們來(lái)繼續(xù)往下跟蹤柿赊,我們深入跟蹤下剛才自定義獨(dú)占鎖lock()方法里面acquire()的實(shí)現(xiàn)
publicfinalvoidacquire(intarg){
if(!tryAcquire(arg)&&
acquireQueued(addWaiter(Node.EXCLUSIVE),arg))
selfInterrupt();
}
這個(gè)方法在AQS類里面俩功,看到里面的tryAcquire(arg)大家也就明白了,tryAcquire(arg)方法獲取同步狀態(tài)碰声,后面acquireQueued(addWaiter(Node.EXCLUSIVE),?arg)方法就是說(shuō)的節(jié)點(diǎn)構(gòu)造诡蜓、加入同步隊(duì)列及在同步隊(duì)列中自旋等待的AQS沒(méi)暴露給我們的相關(guān)操作。大體的流程就是首先調(diào)用自定義同步器實(shí)現(xiàn)的tryAcquire()方法胰挑,該方法保證線程安全的獲取同步狀態(tài)蔓罚,如果獲取同步狀態(tài)失敗,則構(gòu)造同步節(jié)點(diǎn)(獨(dú)占式Node.EXCLUSIVE瞻颂,同一時(shí)刻只能有一個(gè)線程成功獲取同步狀態(tài))并通過(guò)addWaiter()方法將該節(jié)點(diǎn)加入到同步隊(duì)列的尾部豺谈,最后調(diào)用acquireQueued()方法,使得該節(jié)點(diǎn)以“死循環(huán)”的方式獲取同步狀態(tài)贡这。如果獲取不到則阻塞節(jié)點(diǎn)中的線程茬末,而被阻塞線程的喚醒主要靠前驅(qū)節(jié)點(diǎn)的出隊(duì)或阻塞線程被中斷來(lái)實(shí)現(xiàn)。也許你還是不明白剛才所說(shuō)的盖矫,那么我們繼續(xù)跟蹤下addWaiter()方法的實(shí)現(xiàn)
privateNodeaddWaiter(Nodemode){
Nodenode=newNode(Thread.currentThread(),mode);
// Try the fast path of enq; backup to full enq on failure
Nodepred=tail;
if(pred!=null){
node.prev=pred;
if(compareAndSetTail(pred,node)){
pred.next=node;
returnnode;
}
}
enq(node);
returnnode;
}
privateNodeenq(finalNodenode){
for(;;){
Nodet=tail;
if(t==null){// Must initialize
if(compareAndSetHead(newNode()))
tail=head;
}else{
node.prev=t;
if(compareAndSetTail(t,node)){
t.next=node;
returnt;
}
}
}
}
上面的代碼通過(guò)使用compareAndSetTail()方法來(lái)確保節(jié)點(diǎn)能夠被線程安全添加丽惭。在enq()方法中击奶,同步器通過(guò)“死循環(huán)”來(lái)確保節(jié)點(diǎn)的正確添加,在”死循環(huán)“中只有通過(guò)CAS將節(jié)點(diǎn)設(shè)置成為尾節(jié)點(diǎn)之后责掏,當(dāng)前線程才能夠從該方法返回正歼,否則,當(dāng)前線程不斷地嘗試重試設(shè)置拷橘。
在節(jié)點(diǎn)進(jìn)入同步隊(duì)列之后局义,發(fā)生了什么呢?現(xiàn)在我們繼續(xù)跟蹤下acquireQueued()方法
finalbooleanacquireQueued(finalNodenode,intarg){
booleanfailed=true;
try{
booleaninterrupted=false;
for(;;){
finalNodep=node.predecessor();
if(p==head&&tryAcquire(arg)){
setHead(node);
p.next=null;// help GC
failed=false;
returninterrupted;
}
if(shouldParkAfterFailedAcquire(p,node)&&
parkAndCheckInterrupt())
interrupted=true;
}
}finally{
if(failed)
cancelAcquire(node);
}
}
從上面的代碼我們不難看出冗疮,節(jié)點(diǎn)進(jìn)入同步隊(duì)列之后萄唇,就進(jìn)入了一個(gè)自旋的過(guò)程,每個(gè)節(jié)點(diǎn)(或者說(shuō)每個(gè)線程)都在自省的觀察术幔,當(dāng)條件滿足時(shí)(自己的前驅(qū)節(jié)點(diǎn)是頭節(jié)點(diǎn)就進(jìn)行CAS設(shè)置同步狀態(tài))就獲得同步狀態(tài)另萤,然后就可以從自旋的過(guò)程中退出,否則依舊在這個(gè)自旋的過(guò)程中诅挑。
collections
從前面的思維導(dǎo)圖我們可以看到并發(fā)容器包括鏈表四敞、隊(duì)列、HashMap等.它們都是線程安全的.
ConcurrentHashMap : 一個(gè)高效的線程安全的HashMap拔妥。
CopyOnWriteArrayList : 在讀多寫少的場(chǎng)景中,性能非常好,遠(yuǎn)遠(yuǎn)高于vector忿危。
ConcurrentLinkedQueue : 高效并發(fā)隊(duì)列,使用鏈表實(shí)現(xiàn),可以看成線程安全的LinkedList。
BlockingQueue : 一個(gè)接口,JDK內(nèi)部通過(guò)鏈表,數(shù)組等方式實(shí)現(xiàn)了這個(gè)接口,表示阻塞隊(duì)列,非常適合用作數(shù)據(jù)共享 没龙。
ConcurrentSkipListMap : 跳表的實(shí)現(xiàn),這是一個(gè)Map,使用跳表數(shù)據(jù)結(jié)構(gòu)進(jìn)行快速查找 铺厨。
另外Collections工具類可以幫助我們將任意集合包裝成線程安全的集合。在這里重點(diǎn)說(shuō)下ConcurrentHashMap和BlockingQueue這兩個(gè)并發(fā)容器硬纤。
我們都知道HashMap線程不安全的解滓,而我們可以通過(guò)Collections.synchronizedMap(new HashMap<>())來(lái)包裝一個(gè)線程安全的HashMap或者使用線程安全的HashTable,但是它們的效率都不是很好筝家,這時(shí)候我們就有了ConcurrentHashMap洼裤。為什么ConcurrentHashMap高效且線程安全呢?其實(shí)它使用了鎖分段技術(shù)來(lái)提高了并發(fā)的訪問(wèn)率溪王。假如容器里有多把鎖腮鞍,每一把鎖用于鎖容器的一部分?jǐn)?shù)據(jù),那么當(dāng)多線程訪問(wèn)容器里不同數(shù)據(jù)段的數(shù)據(jù)時(shí)在扰,線程間就不會(huì)存在鎖競(jìng)爭(zhēng)缕减,從而可以有效地提高并發(fā)訪問(wèn)效率,這就是鎖分段技術(shù)芒珠。首先將數(shù)據(jù)分成一段段的存儲(chǔ)桥狡,然后給每段數(shù)據(jù)配一把鎖,當(dāng)一個(gè)線程占用鎖訪問(wèn)其中一個(gè)段數(shù)據(jù)的時(shí)候,其他段的數(shù)據(jù)也能被其他線程訪問(wèn)裹芝。而既然數(shù)據(jù)被分成了多個(gè)段部逮,線程如何定位要訪問(wèn)的段的數(shù)據(jù)呢?這里其實(shí)是通過(guò)散列算法來(lái)定位的嫂易。
現(xiàn)在來(lái)談?wù)勛枞?duì)列兄朋,阻塞隊(duì)列其實(shí)跟后面要談的線程池息息相關(guān)的,JDK7提供了7個(gè)阻塞隊(duì)列怜械,分別是
ArrayBlockingQueue :一個(gè)由數(shù)組結(jié)構(gòu)組成的有界阻塞隊(duì)列颅和。
LinkedBlockingQueue :一個(gè)由鏈表結(jié)構(gòu)組成的有界阻塞隊(duì)列。
PriorityBlockingQueue :一個(gè)支持優(yōu)先級(jí)排序的無(wú)界阻塞隊(duì)列缕允。
DelayQueue:一個(gè)使用優(yōu)先級(jí)隊(duì)列實(shí)現(xiàn)的無(wú)界阻塞隊(duì)列峡扩。
SynchronousQueue:一個(gè)不存儲(chǔ)元素的阻塞隊(duì)列。
LinkedTransferQueue:一個(gè)由鏈表結(jié)構(gòu)組成的無(wú)界阻塞隊(duì)列障本。
LinkedBlockingDeque:一個(gè)由鏈表結(jié)構(gòu)組成的雙向阻塞隊(duì)列教届。
如果隊(duì)列是空的,消費(fèi)者會(huì)一直等待驾霜,當(dāng)生產(chǎn)者添加元素時(shí)候案训,消費(fèi)者是如何知道當(dāng)前隊(duì)列有元素的呢?如果讓你來(lái)設(shè)計(jì)阻塞隊(duì)列你會(huì)如何設(shè)計(jì)粪糙,讓生產(chǎn)者和消費(fèi)者能夠高效率的進(jìn)行通訊呢强霎?讓我們先來(lái)看看JDK是如何實(shí)現(xiàn)的。
使用通知模式實(shí)現(xiàn)猜旬。所謂通知模式脆栋,就是當(dāng)生產(chǎn)者往滿的隊(duì)列里添加元素時(shí)會(huì)阻塞住生產(chǎn)者,當(dāng)消費(fèi)者消費(fèi)了一個(gè)隊(duì)列中的元素后洒擦,會(huì)通知生產(chǎn)者當(dāng)前隊(duì)列可用。通過(guò)查看JDK源碼發(fā)現(xiàn)ArrayBlockingQueue使用了Condition來(lái)實(shí)現(xiàn)怕膛,代碼如下:
privatefinalConditionnotFull;
privatefinalConditionnotEmpty;
publicArrayBlockingQueue(intcapacity,booleanfair){
//省略其他代碼
notEmpty=lock.newCondition();
notFull=lock.newCondition();
}
publicvoidput(E e)throwsInterruptedException{
checkNotNull(e);
finalReentrantLocklock=this.lock;
lock.lockInterruptibly();
try{
while(count==items.length)
notFull.await();
insert(e);
}finally{
lock.unlock();
}
}
publicE take()throwsInterruptedException{
finalReentrantLocklock=this.lock;
lock.lockInterruptibly();
try{
while(count==0)
notEmpty.await();
returnextract();
}finally{
lock.unlock();
}
}
privatevoidinsert(E x){
items[putIndex]=x;
putIndex=inc(putIndex);
++count;
notEmpty.signal();
}
當(dāng)我們往隊(duì)列里插入一個(gè)元素時(shí)熟嫩,如果隊(duì)列不可用,阻塞生產(chǎn)者主要通過(guò)LockSupport.park(this)來(lái)實(shí)現(xiàn)
publicfinalvoidawait()throwsInterruptedException{
if(Thread.interrupted())
thrownewInterruptedException();
Nodenode=addConditionWaiter();
intsavedState=fullyRelease(node);
intinterruptMode=0;
while(!isOnSyncQueue(node)){
LockSupport.park(this);
if((interruptMode=checkInterruptWhileWaiting(node))!=0)
break;
}
if(acquireQueued(node,savedState)&&interruptMode!=THROW_IE)
interruptMode=REINTERRUPT;
if(node.nextWaiter!=null)// clean up if cancelled
unlinkCancelledWaiters();
if(interruptMode!=0)
reportInterruptAfterWait(interruptMode);
}
繼續(xù)進(jìn)入源碼褐捻,發(fā)現(xiàn)調(diào)用setBlocker先保存下將要阻塞的線程掸茅,然后調(diào)用unsafe.park阻塞當(dāng)前線程。
publicstaticvoidpark(Objectblocker){
Threadt=Thread.currentThread();
setBlocker(t,blocker);
unsafe.park(false,0L);
setBlocker(t,null);
}
unsafe.park是個(gè)native方法柠逞,代碼如下:
publicnativevoidpark(booleanisAbsolute,longtime);
park這個(gè)方法會(huì)阻塞當(dāng)前線程昧狮,只有以下四種情況中的一種發(fā)生時(shí),該方法才會(huì)返回板壮。
與park對(duì)應(yīng)的unpark執(zhí)行或已經(jīng)執(zhí)行時(shí)逗鸣。注意:已經(jīng)執(zhí)行是指unpark先執(zhí)行,然后再執(zhí)行的park。
線程被中斷時(shí)撒璧。
如果參數(shù)中的time不是零透葛,等待了指定的毫秒數(shù)時(shí)。
發(fā)生異城溆#現(xiàn)象時(shí)僚害。這些異常事先無(wú)法確定。
我們繼續(xù)看一下JVM是如何實(shí)現(xiàn)park方法的繁调,park在不同的操作系統(tǒng)使用不同的方式實(shí)現(xiàn)萨蚕,在linux下是使用的是系統(tǒng)方法pthread_cond_wait實(shí)現(xiàn)。實(shí)現(xiàn)代碼在JVM源碼路徑src/os/linux/vm/os_linux.cpp里的 os::PlatformEvent::park方法蹄胰,代碼如下:
voidos::PlatformEvent::park(){
intv;
for(;;){
v=_Event;
if(Atomic::cmpxchg(v-1,&_Event,v)==v)break;
}
guarantee(v>=0,"invariant");
if(v==0){
// Do this the hard way by blocking ...
intstatus=pthread_mutex_lock(_mutex);
assert_status(status==0,status,"mutex_lock");
guarantee(_nParked==0,"invariant");
++_nParked;
while(_Event<0){
status=pthread_cond_wait(_cond,_mutex);
// for some reason, under 2.7 lwp_cond_wait() may return ETIME ...
// Treat this the same as if the wait was interrupted
if(status==ETIME){status=EINTR;}
assert_status(status==0||status==EINTR,status,"cond_wait");
}
--_nParked;
// In theory we could move the ST of 0 into _Event past the unlock(),
// but then we'd need a MEMBAR after the ST.
_Event=0;
status=pthread_mutex_unlock(_mutex);
assert_status(status==0,status,"mutex_unlock");
}
guarantee(_Event>=0,"invariant");
}
}
pthread_cond_wait是一個(gè)多線程的條件變量函數(shù)门岔,cond是condition的縮寫,字面意思可以理解為線程在等待一個(gè)條件發(fā)生烤送,這個(gè)條件是一個(gè)全局變量寒随。這個(gè)方法接收兩個(gè)參數(shù),一個(gè)共享變量_cond帮坚,一個(gè)互斥量_mutex妻往。而unpark方法在linux下是使用pthread_cond_signal實(shí)現(xiàn)的。park 在windows下則是使用WaitForSingleObject實(shí)現(xiàn)的试和。
當(dāng)隊(duì)列滿時(shí)讯泣,生產(chǎn)者往阻塞隊(duì)列里插入一個(gè)元素,生產(chǎn)者線程會(huì)進(jìn)入WAITING (parking)狀態(tài)阅悍。
executor
Executor框架提供了各種類型的線程池好渠,不同的線程池應(yīng)用了前面介紹的不同的堵塞隊(duì)列
Executor框架最核心的類是ThreadPoolExecutor,它是線程池的實(shí)現(xiàn)類节视。 對(duì)于核心的幾個(gè)線程池拳锚,無(wú)論是newFixedThreadPool()、newSingleThreadExecutor()還是newCacheThreadPool()方法寻行,雖然看起來(lái)創(chuàng)建的線程具有完全不同的功能特點(diǎn)霍掺,但其內(nèi)部均使用了ThreadPoolExecutor實(shí)現(xiàn)
publicstaticExecutorServicenewFixedThreadPool(intnThreads){
returnnewThreadPoolExecutor(nThreads,nThreads,
0L,TimeUnit.MILLISECONDS,
newLinkedBlockingQueue());
}
publicstaticExecutorServicenewSingleThreadExecutor(){
returnnewFinalizableDelegatedExecutorService
(newThreadPoolExecutor(1,1,
0L,TimeUnit.MILLISECONDS,
newLinkedBlockingQueue()));
}
publicstaticExecutorServicenewCachedThreadPool(){
returnnewThreadPoolExecutor(0,Integer.MAX_VALUE,
60L,TimeUnit.SECONDS,
newSynchronousQueue());
}
newFixedThreadPool()方法的實(shí)現(xiàn),它返回一個(gè)corePoolSize和maximumPoolSize一樣的拌蜘,并使用了LinkedBlockingQueue任務(wù)隊(duì)列(無(wú)界隊(duì)列)的線程池杆烁。當(dāng)任務(wù)提交非常頻繁時(shí),該隊(duì)列可能迅速膨脹简卧,從而系統(tǒng)資源耗盡兔魂。
newSingleThreadExecutor()返回單線程線程池,是newFixedThreadPool()方法的退化举娩,只是簡(jiǎn)單的將線程池?cái)?shù)量設(shè)置為1析校。
newCachedThreadPool()方法返回corePoolSize為0而maximumPoolSize無(wú)窮大的線程池构罗,這意味著沒(méi)有任務(wù)的時(shí)候線程池內(nèi)沒(méi)有現(xiàn)場(chǎng),而當(dāng)任務(wù)提交時(shí)勺良,該線程池使用空閑線程執(zhí)行任務(wù)绰播,若無(wú)空閑則將任務(wù)加入SynchronousQueue隊(duì)列,而SynchronousQueue隊(duì)列是直接提交隊(duì)列尚困,它總是破事線程池增加新的線程來(lái)執(zhí)行任務(wù)蠢箩。當(dāng)任務(wù)執(zhí)行完后由于corePoolSize為0,因此空閑線程在指定時(shí)間內(nèi)(60s)被回收事甜。對(duì)于newCachedThreadPool()谬泌,如果有大量任務(wù)提交,而任務(wù)又不那么快執(zhí)行時(shí)逻谦,那么系統(tǒng)變回開(kāi)啟等量的線程處理掌实,這樣做法可能會(huì)很快耗盡系統(tǒng)的資源,因?yàn)樗鼤?huì)增加無(wú)窮大數(shù)量的線程贱鼻。
由以上線程池的實(shí)現(xiàn)可以看到,它們都只是ThreadPoolExecutor類的封裝滋将。我們看下ThreadPoolExecutor最重要的構(gòu)造函數(shù):
publicThreadPoolExecutor(
//核心線程池邻悬,指定了線程池中的線程數(shù)量
intcorePoolSize,
//基本線程池,指定了線程池中的最大線程數(shù)量
intmaximumPoolSize,
//當(dāng)前線程池?cái)?shù)量超過(guò)corePoolSize時(shí)随闽,多余的空閑線程的存活時(shí)間父丰,即多次時(shí)間內(nèi)會(huì)被銷毀。
longkeepAliveTime,
//keepAliveTime的單位
TimeUnitunit,
//任務(wù)隊(duì)列掘宪,被提交但尚未被執(zhí)行的任務(wù)蛾扇。
BlockingQueueworkQueue,
//線程工廠,用于創(chuàng)建線程魏滚,一般用默認(rèn)的即可
ThreadFactorythreadFactory,
//拒絕策略镀首,當(dāng)任務(wù)太多來(lái)不及處理,如何拒絕任務(wù)栏赴。
RejectedExecutionHandlerhandler)
ThreadPoolExecutor的任務(wù)調(diào)度邏輯如下
從上圖我們可以看出蘑斧,當(dāng)提交一個(gè)新任務(wù)到線程池時(shí),線程池的處理流程如下:
首先線程池判斷基本線程池是否已滿须眷,如果沒(méi)滿,創(chuàng)建一個(gè)工作線程來(lái)執(zhí)行任務(wù)沟突。滿了花颗,則進(jìn)入下個(gè)流程惠拭。
其次線程池判斷工作隊(duì)列是否已滿扩劝,如果沒(méi)滿庸论,則將新提交的任務(wù)存儲(chǔ)在工作隊(duì)列里。滿了棒呛,則進(jìn)入下個(gè)流程聂示。
最后線程池判斷整個(gè)線程池是否已滿,如果沒(méi)滿簇秒,則創(chuàng)建一個(gè)新的工作線程來(lái)執(zhí)行任務(wù)鱼喉,滿了,則交給飽和策略來(lái)處理這個(gè)任務(wù)趋观。
下面我們來(lái)看看ThreadPoolExecutor核心調(diào)度代碼
publicvoidexecute(Runnablecommand){
if(command==null)
thrownewNullPointerException();
intc=ctl.get();
/**
* workerCountOf(c)獲取當(dāng)前線程池線程總數(shù)
* 當(dāng)前線程數(shù)小于corePoolSize核心線程數(shù)時(shí)扛禽,會(huì)將任務(wù)通過(guò)addWorker(command, true)方法直接調(diào)度執(zhí)行。
* 否則進(jìn)入下個(gè)if皱坛,將任務(wù)加入等待隊(duì)列
**/
if(workerCountOf(c)
if(addWorker(command,true))
return;
c=ctl.get();
}
/**
* workQueue.offer(command) 將任務(wù)加入等待隊(duì)列编曼。
* 如果加入失敗(比如有界隊(duì)列達(dá)到上限或者使用了synchronousQueue)則會(huì)執(zhí)行else剩辟。
*
**/
if(isRunning(c)&&workQueue.offer(command)){
intrecheck=ctl.get();
if(!isRunning(recheck)&&remove(command))
reject(command);
elseif(workerCountOf(recheck)==0)
addWorker(null,false);
}
/**
* addWorker(command, false)直接交給線程池掐场,
* 如果當(dāng)前線程已達(dá)到maximumPoolSize,則提交失敗執(zhí)行reject()拒絕策略贩猎。
**/
elseif(!addWorker(command,false))
reject(command);
}
從上面的源碼我們可以知道execute的執(zhí)行步驟:
如果當(dāng)前運(yùn)行的線程少于corePoolSize熊户,則創(chuàng)建新線程來(lái)執(zhí)行任務(wù)(注意,執(zhí)行這一步驟需要獲取全局鎖)融欧。
如果運(yùn)行的線程等于或多于corePoolSize敏弃,則將任務(wù)加入到BlockingQueue。
如果無(wú)法將任務(wù)假如BlockingQueue(隊(duì)列已滿)噪馏,則創(chuàng)建新的線程來(lái)處理任務(wù)(注意麦到,執(zhí)行這一步驟需要獲取全局鎖)。
如果創(chuàng)建新線程將使當(dāng)前運(yùn)行的線程超出maximumPoolSize欠肾,任務(wù)將被拒絕瓶颠,并調(diào)用RejectedExecutionHandler.rejectedExecution()方法。
ThreadPoolExecutor采取上述步驟的總體設(shè)計(jì)思路刺桃,是為了在執(zhí)行execute()方法時(shí)粹淋,盡可能的避免獲取全局鎖(那將會(huì)是一個(gè)嚴(yán)重的 可伸縮瓶頸)。在ThreadPoolExecutor完成預(yù)熱之后(當(dāng)前運(yùn)行的線程數(shù)大于等于corePoolSize)瑟慈,幾乎所有的execute()方法調(diào)用都是執(zhí)行步驟2桃移,而步驟2不需要獲取全局鎖。
參考閱讀
本文部分內(nèi)容參考自《Java并發(fā)編程的藝術(shù)》葛碧、《深入理解java虛擬機(jī)(第2版)》借杰、《實(shí)戰(zhàn)Java高并發(fā)程序設(shè)計(jì)》、《深入Java內(nèi)存模型》进泼、《Java并發(fā)編程實(shí)踐》蔗衡,感興趣的可自行查閱纤虽。
打開(kāi)手機(jī)看微信,關(guān)注微信訂閱號(hào)“Martin說(shuō)”绞惦,每天收獲更多Martin的閑扯逼纸。