Java并發(fā)機(jī)制的底層實(shí)現(xiàn)

Java代碼編譯后會(huì)變成Java字節(jié)碼辈毯,字節(jié)碼被類加載器加載到JVM里仪芒,JVM執(zhí)行字節(jié)碼啃炸,最終需要轉(zhuǎn)化為匯編指令在CPU上執(zhí)行,Java中所使用的并發(fā)機(jī)制依賴于JVM的實(shí)現(xiàn)和CPU的指令融击,現(xiàn)在我們一起來探索下Java并發(fā)機(jī)制的底層實(shí)現(xiàn)原理拇涤,主要內(nèi)容有volatile惩坑、synchronized、原子操作實(shí)現(xiàn)原理。

(1)volatile

當(dāng)變量被定位volatile之后,它將具備兩種特性:
可見性:保證此變量對(duì)所有線程的可見性狭魂,可見性指當(dāng)一條線程修改了這個(gè)變量的值之后,新值對(duì)于其他線程來說是可以立即得知的。而普通變量的值在線程之間傳遞需要通過主內(nèi)存來完成。
指令重排序:第二個(gè)語義就是禁止指令重排序。volatile關(guān)鍵字禁止指令重排序有兩層意思:
1)當(dāng)程序執(zhí)行到volatile變量的讀操作或者寫操作時(shí)论巍,在其前面的操作的更改肯定全部已經(jīng)進(jìn)行,且結(jié)果已經(jīng)對(duì)后面的操作可見郑现;在其后面的操作肯定還沒有進(jìn)行湃崩;
2)在進(jìn)行指令優(yōu)化時(shí)攒读,不能將在對(duì)volatile變量訪問的語句放在其后面執(zhí)行邓梅,也不能把volatile變量后面的語句放到其前面執(zhí)行。
問題一:volatile如何保證可見性?
即一個(gè)線程修改了值尸红,另外一個(gè)線程可以立馬可見吱涉,java內(nèi)存模型對(duì)volatile變量的操作指定如何規(guī)則來保證可見性:
(1)每次使用變量前都必須先從主內(nèi)存刷新最新的值,用于保證能看見其他線程對(duì)變量v所做的修改级乐;(這個(gè)寫回內(nèi)存的操作會(huì)導(dǎo)致在其它CPU里緩存了該內(nèi)存地址的數(shù)據(jù)無效)
(2)在工作內(nèi)存中,每次修改變量都必須立即同步回主內(nèi)存中县匠,用于保證其他線程可以看到自己對(duì)變量的修改风科;
問題二:volatile如何禁止指令重排序?
觀察加入volatile關(guān)鍵字和沒有加入volatile關(guān)鍵字時(shí)所生成的字節(jié)碼發(fā)現(xiàn)乞旦,加入volatile關(guān)鍵字時(shí)贼穆,會(huì)多出一個(gè)lock前綴指令,lock前綴指令實(shí)際上相當(dāng)于一個(gè)內(nèi)存屏障兰粉,在volatile修飾的變量進(jìn)行賦值后故痊,會(huì)添加lock操作(內(nèi)存屏障),內(nèi)存屏障會(huì)提供3個(gè)功能:
1)它確保指令重排序時(shí)不會(huì)把其后面的指令排到內(nèi)存屏障之前的位置玖姑,也不會(huì)把前面的指令排到內(nèi)存屏障的后面愕秫;即在執(zhí)行到內(nèi)存屏障這句指令時(shí),在它前面的操作已經(jīng)全部完成焰络;
2)它會(huì)強(qiáng)制將對(duì)緩存的修改操作立即寫入主存戴甩;
3)如果是寫操作,它會(huì)導(dǎo)致其他CPU中對(duì)應(yīng)的緩存行無效闪彼。
簡(jiǎn)單例子:

//x甜孤、y為非volatile變量
//flag為volatile變量
x = 2;        //語句1
y = 0;        //語句2
flag = true;  //語句3
x = 4;         //語句4
y = -1;       //語句5

由于flag變量為volatile變量,那么在進(jìn)行指令重排序的過程的時(shí)候畏腕,不會(huì)將語句3放到語句1缴川、語句2前面,也不會(huì)講語句3放到語句4描馅、語句5后面把夸。但是要注意語句1和語句2的順序、語句4和語句5的順序是不作任何保證的流昏。并且volatile關(guān)鍵字能保證扎即,執(zhí)行到語句3時(shí)吞获,語句1和語句2必定是執(zhí)行完畢了的,且語句1和語句2的執(zhí)行結(jié)果對(duì)語句3谚鄙、語句4各拷、語句5是可見的。
新例子:

//線程1:
context = loadContext();   //語句1
inited = true;             //語句2
 
//線程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

指令重排序會(huì)導(dǎo)致語句2會(huì)在語句1之前執(zhí)行闷营,那么就可能導(dǎo)致context還沒被初始化烤黍,而線程2中就使用未初始化的context去進(jìn)行操作,導(dǎo)致程序出錯(cuò)傻盟。這里如果用volatile關(guān)鍵字對(duì)inited變量進(jìn)行修飾速蕊,就不會(huì)出現(xiàn)這種問題了,因?yàn)楫?dāng)執(zhí)行到語句2時(shí)娘赴,必定能保證context已經(jīng)初始化完畢规哲。

(二)synchronized的實(shí)現(xiàn)原理

synchronized有三種使用形式,修飾同步方法(靜態(tài)方法和實(shí)例方法)和修飾代碼塊诽表。使用javap查看字節(jié)碼唉锌,發(fā)現(xiàn)對(duì)于同步方法,JVM采用ACC_SYNCHRONIZED標(biāo)記符來實(shí)現(xiàn)同步竿奏。對(duì)于同步代碼塊袄简。JVM采用monitorenter、monitorexit兩個(gè)指令來實(shí)現(xiàn)同步泛啸。synchronized使用場(chǎng)景如下圖所示:

synchronized使用場(chǎng)景.png

synchronized修飾同步方法原理:同步方法的常量池中會(huì)有一個(gè)ACC_SYNCHRONIZED標(biāo)志绿语。當(dāng)某個(gè)線程要訪問某個(gè)方法的時(shí)候,會(huì)檢查是否有ACC_SYNCHRONIZED候址,如果有設(shè)置吕粹,則需要先獲得監(jiān)視器鎖,然后開始執(zhí)行方法岗仑,方法執(zhí)行之后再釋放監(jiān)視器鎖昂芜。這時(shí)如果其他線程來請(qǐng)求執(zhí)行方法,會(huì)因?yàn)闊o法獲得監(jiān)視器鎖而被阻斷住赔蒲。值得注意的是泌神,如果在方法執(zhí)行過程中,發(fā)生了異常舞虱,并且方法內(nèi)部并沒有處理該異常欢际,那么在異常被拋到方法外面之前監(jiān)視器鎖會(huì)被自動(dòng)釋放。
synchronized修飾同步代碼塊原理:synchronized修飾同步代碼塊時(shí)矾兜,經(jīng)過編譯之后损趋,會(huì)在同步代碼塊前后形成monitorenter和monitorexit兩個(gè)字節(jié)碼指令,這兩個(gè)字節(jié)碼都需要一個(gè)reference類型的參數(shù)來指明要鎖定和解鎖的對(duì)象椅寺,如果java程序中synchronized明確指定了對(duì)象參數(shù)浑槽,那就是這個(gè)對(duì)象的reference蒋失,如果沒有明確指定,那就根據(jù)synchronized修飾的實(shí)例方法和類方法桐玻,去取對(duì)應(yīng)的對(duì)象實(shí)例或者Class對(duì)象作為鎖對(duì)象篙挽。執(zhí)行monitorenter指令時(shí),需要嘗試獲取對(duì)象的鎖镊靴,如果這個(gè)對(duì)象沒有鎖定铣卡,或者當(dāng)前線程已經(jīng) 擁有了那個(gè)對(duì)象的鎖,把鎖計(jì)數(shù)器加1偏竟,響應(yīng)的在執(zhí)行monitorexit指令時(shí)煮落,會(huì)將鎖計(jì)數(shù)器減1,當(dāng)計(jì)數(shù)器為0時(shí)踊谋,鎖就被釋放蝉仇,如果獲取鎖對(duì)象失敗,那么當(dāng)前線程就要阻塞等待殖蚕,知道對(duì)象鎖被另外一個(gè)線程釋放位置量淌。synchronized對(duì)象鎖與對(duì)象的內(nèi)存布局中(對(duì)象頭、實(shí)例數(shù)據(jù)嫌褪、對(duì)其填充)對(duì)象頭有關(guān)系。
synchronized原理簡(jiǎn)單總結(jié):同步方法通過ACC_SYNCHRONIZED關(guān)鍵字隱式的對(duì)方法進(jìn)行加鎖胚股。當(dāng)線程要執(zhí)行的方法被標(biāo)注上ACC_SYNCHRONIZED時(shí)笼痛,需要先獲得鎖才能執(zhí)行該方法。同步代碼塊通過monitorenter和monitorexit執(zhí)行來進(jìn)行加鎖琅拌。當(dāng)線程執(zhí)行到monitorenter的時(shí)候要先獲得所鎖缨伊,才能執(zhí)行后面的方法。當(dāng)線程執(zhí)行到monitorexit的時(shí)候則要釋放鎖进宝。
當(dāng)一個(gè)線程試圖訪問同步代碼塊時(shí)刻坊,它必須首先獲取鎖,退出或者拋出異常時(shí)必須釋放鎖党晋,那么鎖到底存放在那谭胚?鎖里會(huì)存儲(chǔ)什么信息那?
1.鎖到底存放在那未玻?
需要從Java對(duì)象的內(nèi)存布局說起灾而,Java對(duì)象內(nèi)存布局分為三塊區(qū)域:對(duì)象頭、實(shí)例數(shù)據(jù)扳剿、對(duì)齊填充旁趟。
對(duì)象頭:主要存儲(chǔ)了2部分信息,第一部分是對(duì)象自身運(yùn)行的數(shù)據(jù)庇绽,如hashcode锡搜,GC分代橙困、鎖狀態(tài)標(biāo)志、線程持有的鎖耕餐、偏向線程ID等信息凡傅;
第二部分是類型指針,就是對(duì)象對(duì)它的類元數(shù)據(jù)指針蛾方,其實(shí)就是一個(gè)引用像捶。虛擬機(jī)通過這個(gè)指針(引用)來確定對(duì)象是哪個(gè)類的實(shí)例。
實(shí)例數(shù)據(jù)(Instance Data):對(duì)象真正存儲(chǔ)的有效信息桩砰,也就是程序代碼中所寫的各種類型的字段內(nèi)容拓春。
對(duì)齊填充(Padding):這個(gè)不是必然存在的。HotSpot虛擬機(jī)自動(dòng)內(nèi)存管理要求對(duì)象起始地址必須是8字節(jié)的整數(shù)倍亚隅,也就是對(duì)象大小必須是8字節(jié)的整數(shù)倍硼莽,對(duì)象實(shí)例數(shù)據(jù)部分沒有對(duì)齊時(shí),需要通過對(duì)齊填充來補(bǔ)齊煮纵。
因此懂鸵,synchronized用的鎖存放在Java對(duì)象頭中。
2.鎖里會(huì)存儲(chǔ)什么信息行疏?
為了減少獲得鎖和釋放鎖帶來的性能消耗匆光,虛擬機(jī)開發(fā)團(tuán)隊(duì)花費(fèi)了大量的精力去實(shí)現(xiàn)各種鎖優(yōu)化技術(shù)。鎖的狀態(tài)總共有四種酿联,無鎖狀態(tài)终息、偏向鎖、輕量級(jí)鎖和重量級(jí)鎖贞让。隨著鎖的競(jìng)爭(zhēng)周崭,鎖可以從偏向鎖升級(jí)到輕量級(jí)鎖,再升級(jí)的重量級(jí)鎖喳张,但是鎖的升級(jí)是單向的续镇,也就是說只能從低到高升級(jí),不會(huì)出現(xiàn)鎖的降級(jí)销部。鎖升級(jí)卻不能降級(jí)的策略摸航,目的是為了提高獲得鎖和釋放鎖的效率。要理解輕量級(jí)鎖以及偏向鎖的原理和運(yùn)作過程舅桩,需要從JVM對(duì)象頭了解忙厌,synchronized使用的鎖對(duì)象是存儲(chǔ)在Java對(duì)象頭里。如果對(duì)象是數(shù)組類型使用3個(gè)字寬存儲(chǔ)對(duì)象向頭江咳,如果是非數(shù)據(jù)類型用2字寬存儲(chǔ)對(duì)象頭逢净,在32位虛擬機(jī)中,1字寬等于4字節(jié),即32bit爹土。Java對(duì)象頭結(jié)構(gòu)如下圖所示:

image

MarkWord是實(shí)現(xiàn)偏向鎖和輕量級(jí)鎖的關(guān)鍵.其中Mark Word在默認(rèn)情況下存儲(chǔ)著對(duì)象的HashCode甥雕、分代年齡、鎖標(biāo)記位等胀茵,以下是32位JVM的Mark Word默認(rèn)存儲(chǔ)結(jié)構(gòu)如下圖所示:

image

由于對(duì)象頭的信息是與對(duì)象自身定義的數(shù)據(jù)沒有關(guān)系的額外存儲(chǔ)成本社露,因此考慮到JVM的空間效率,Mark Word 被設(shè)計(jì)成為一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu)琼娘,以便存儲(chǔ)更多有效的數(shù)據(jù)峭弟,它會(huì)根據(jù)對(duì)象本身的狀態(tài)復(fù)用自己的存儲(chǔ)空間在運(yùn)行期間,MarkWork里存儲(chǔ)的數(shù)據(jù)會(huì)隨著鎖標(biāo)志位的變化而變化脱拼,Mark Word可能變化為存儲(chǔ)以下四種數(shù)據(jù)瞒瘸,如下圖所示:

image

(三)原子操作的實(shí)現(xiàn)原理

Java中可以通過CAS和鎖來實(shí)現(xiàn)原子操作。
在java.util.concurrent.atomic包下熄浓,一系列以Atomic開頭的包裝類情臭。例如AtomicBoolean,AtomicInteger赌蔑,AtomicLong俯在。它們分別用于Boolean,Integer娃惯,Long類型的原子性操作跷乐,atomic操作的底層實(shí)現(xiàn)正是利用的CAS機(jī)制、原子類是通過 Unsafe 類中的 CAS 指令從硬件層面來實(shí)現(xiàn)線程安全的趾浅。
CAS機(jī)制:當(dāng)中使用了3個(gè)基本操作數(shù):內(nèi)存地址V愕提,舊的預(yù)期值A(chǔ),要修改的新值B潮孽。更新一個(gè)變量的時(shí)候,只有當(dāng)變量的預(yù)期值A(chǔ)和內(nèi)存地址V當(dāng)中的實(shí)際值相同時(shí)筷黔,才會(huì)將內(nèi)存地址V對(duì)應(yīng)的值修改為B往史。
CAS原理:CAS操作會(huì)調(diào)用Unsafe類里邊的compareAndSwapInt()和compareAndSwapLong()等幾個(gè)方法,虛擬機(jī)內(nèi)部對(duì)這些方法做了特殊處理佛舱,即編譯出來的結(jié)果對(duì)應(yīng)一條平臺(tái)相關(guān)的處理器CAS指令椎例,即比較交換操作是一個(gè)原子操作。JVM中CAS操作利用了處理器提供的CMPXCHG指令實(shí)現(xiàn)请祖。
CAS在并發(fā)機(jī)制中的重要性:
1.Java中Lock鎖的同步狀態(tài)存放在AQS中订歪,使用一個(gè)int成員變量來表示同步狀態(tài),通過CAS來修改同步狀態(tài)肆捕,從而保證并發(fā)安全性刷晋。
2.Java中原子操作類底層也是通過CAS來實(shí)現(xiàn)的。
3.sychronized鎖機(jī)制也是基于CAS來實(shí)現(xiàn)的,synchronized使用的鎖存放在Java對(duì)象頭中眼虱,在運(yùn)行期間對(duì)象頭中的Mark Word里存儲(chǔ)的數(shù)據(jù)會(huì)隨著鎖標(biāo)志位的變化而變化喻奥,synchronized在獲取鎖的時(shí)候,使用CAS來修改Mark Word捏悬,從而來獲取鎖撞蚕,即一個(gè)線程想進(jìn)入同步塊的時(shí)候需要使用CAS來獲取鎖,同時(shí)當(dāng)它退出同步塊的時(shí)候需要使用CAS來釋放鎖过牙。從這個(gè)角度來看甥厦,synchronized實(shí)際同步鎖也是基于CAS來實(shí)現(xiàn)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末寇钉,一起剝皮案震驚了整個(gè)濱河市刀疙,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌摧莽,老刑警劉巖庙洼,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異镊辕,居然都是意外死亡油够,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門征懈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來石咬,“玉大人,你說我怎么就攤上這事卖哎」碛疲” “怎么了?”我有些...
    開封第一講書人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵亏娜,是天一觀的道長(zhǎng)焕窝。 經(jīng)常有香客問我,道長(zhǎng)维贺,這世上最難降的妖魔是什么它掂? 我笑而不...
    開封第一講書人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮溯泣,結(jié)果婚禮上虐秋,老公的妹妹穿的比我還像新娘。我一直安慰自己垃沦,他們只是感情好客给,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著肢簿,像睡著了一般靶剑。 火紅的嫁衣襯著肌膚如雪蜻拨。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,763評(píng)論 1 307
  • 那天抬虽,我揣著相機(jī)與錄音官觅,去河邊找鬼。 笑死阐污,一個(gè)胖子當(dāng)著我的面吹牛休涤,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播笛辟,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼功氨,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了手幢?” 一聲冷哼從身側(cè)響起捷凄,我...
    開封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎围来,沒想到半個(gè)月后跺涤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡监透,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年桶错,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片胀蛮。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡院刁,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出粪狼,到底是詐尸還是另有隱情退腥,我是刑警寧澤,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布再榄,位于F島的核電站狡刘,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏困鸥。R本人自食惡果不足惜嗅蔬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望窝革。 院中可真熱鬧购城,春花似錦吕座、人聲如沸虐译。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽漆诽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間厢拭,已是汗流浹背兰英。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留供鸠,地道東北人畦贸。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像楞捂,于是被迫代替她去往敵國(guó)和親薄坏。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容