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修飾同步方法原理:同步方法的常量池中會(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)如下圖所示:
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)如下圖所示:
由于對(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ù)瞒瘸,如下圖所示:
(三)原子操作的實(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)。