本文后面內(nèi)容來自《深入理解java虛擬機(jī)》一文,這本文感覺就像jvm圣經(jīng)一般,值得深入理解。
一、synchronized的特性
- 原子性:原子是世界上的最小單位,具有不可分割性。比如 a=0;(a非long和double類型) 這個(gè)操作是不可分割的,那么我們說這個(gè)操作時(shí)原子操作。再比如:a++; 這個(gè)操作實(shí)際是a = a + 1,它不是一個(gè)原子操作,它是可分割的,它也有并發(fā)問題,即使你加上了volatile關(guān)鍵字,這一點(diǎn)跟volatile不同。
- 可見性:是指線程之間的可見性,一個(gè)線程修改的狀態(tài)對另一個(gè)線程是可見的。也就是一個(gè)線程修改的結(jié)果。另一個(gè)線程馬上就能看到。
- 有序性:即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行,為什么這么說了,因?yàn)閖vm還會對輸入代碼進(jìn)行亂序執(zhí)行(out-of-order Execution)優(yōu)化,處理器會在計(jì)算之后將亂序執(zhí)行的結(jié)果重組,也就是java虛擬機(jī)的即時(shí)編譯器中也有指令重排序優(yōu)化。
- 可重入性:synchronized和ReentrantLock都是可重入鎖。當(dāng)一個(gè)線程試圖操作一個(gè)由其他線程持有的對象鎖的臨界資源時(shí),將會處于阻塞狀態(tài),但當(dāng)一個(gè)線程再次請求自己持有對象鎖的臨界資源時(shí),這種情況屬于重入鎖??芍厝胱畲蟮淖饔檬潜苊馑梨i,如:子類同步方法調(diào)用了父類同步方法,如沒有可重入的特性,則會發(fā)生死鎖;
二、synchronized的用法:
public class SynchronizedTest {
public Object object = new Object();
//成員函數(shù)加鎖,需要獲得當(dāng)前類實(shí)例對象的鎖
public synchronized void add(){
//TODO
}
// 靜態(tài)方法加鎖,需要獲得當(dāng)前類的鎖
public synchronized static void add1(){
//TODO
}
public void method(){
//需要獲取SynchronizedTest類的鎖
synchronized (SynchronizedTest.class){
//TODO
}
//需要獲取object實(shí)例對象的鎖
synchronized (object){
//TODO
}
//需要獲取當(dāng)前類實(shí)例對象的鎖
synchronized (this){
//TODO
}
}
}
三、synchronized鎖的實(shí)現(xiàn)
前面也寫了synchronized有兩種形式上鎖,對方法上鎖和代碼塊。他們都是在進(jìn)入同步代碼之前先獲取鎖,獲取到鎖之后鎖的計(jì)數(shù)器+1,同步代碼執(zhí)行完鎖的計(jì)數(shù)器-1,如果獲取失敗就阻塞式等待鎖的釋放。只是他們在同步塊識別方式上有所不一樣,從class字節(jié)碼文件可以表現(xiàn)出來,一個(gè)是通過方法flags標(biāo)志,一個(gè)是monitorenter和monitorexit指令操作。
查看反編譯

需要注意的是有不止一個(gè)monitorexit呢?其實(shí)后面的monitorexit是來處理異常的,仔細(xì)看反編譯的字節(jié)碼,正常情況下第一個(gè)monitorexit之后會執(zhí)行g(shù)oto指令,也就是return語句,也就是說正常情況下只會執(zhí)行第一個(gè)monitorexit釋放鎖,然后返回。而如果在執(zhí)行中發(fā)生了異常,后面的monitorexit就起作用了,它是由編譯器自動生成的,在發(fā)生異常時(shí)處理異常然后釋放掉鎖。
因?yàn)樵趕ynchronized同步方法我沒找到flags里面多了一個(gè)ACC_SYNCHRONIZED標(biāo)志,這標(biāo)志用來告訴JVM這是一個(gè)同步方法,在進(jìn)入該方法之前先獲取相應(yīng)的鎖,鎖的計(jì)數(shù)器加1,方法結(jié)束后計(jì)數(shù)器-1,如果獲取失敗就阻塞住,知道該鎖被釋放。所以這一塊 先放著,后續(xù)再加上來。
下面這些部分來自《深入理解java虛擬機(jī)》
四、synchronized實(shí)現(xiàn)鎖的基礎(chǔ)

- 實(shí)例數(shù)據(jù):存放類的屬性數(shù)據(jù)信息,包括父類的屬性信息;
- 對齊填充:由于虛擬機(jī)要求 對象起始地址必須是8字節(jié)的整數(shù)倍。填充數(shù)據(jù)不是必須存在的,僅僅是為了字節(jié)對齊;
- 對象頭:這部分拉出來單說。
對象頭
對象頭分為兩部分:
- Mark Word:用于儲存對象自身的運(yùn)行時(shí)數(shù)據(jù),比如哈希碼,GC分代年齡,這部分?jǐn)?shù)據(jù)在32位和64位的虛擬機(jī)中分別為32bit和64bit,是實(shí)現(xiàn)輕量級鎖和偏向鎖的關(guān)鍵。
- Class Metadata Address:這部分用于存儲指向方法區(qū)對象數(shù)據(jù)類型的指針,如果是數(shù)組對象的話,還有一個(gè)額外的部分用于存儲數(shù)組長度。
對象頭信息是與對象自身定義的數(shù)據(jù)無關(guān)的額外存儲成本,但是考慮到虛擬機(jī)的空間效率,Mark Word被設(shè)計(jì)成一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存存儲盡量多的數(shù)據(jù),它會根據(jù)對象的狀態(tài)復(fù)用自己的存儲空間,也就是說,Mark Word會隨著程序的運(yùn)行發(fā)生變化,同時(shí)前面也說過在32bit和64bit上Mark Word儲存結(jié)構(gòu)也不太一樣:
-
32bit
32bit無鎖

關(guān)于鎖標(biāo)志位這部分結(jié)合這張圖來看:

-
64bit
64bit有鎖和無鎖
五、jvm對鎖的優(yōu)化
自旋鎖和自適應(yīng)性自旋鎖
- 自旋鎖:出現(xiàn)背景是因?yàn)閽炱鹁€程和恢復(fù)賢臣改的操作需要轉(zhuǎn)入內(nèi)核態(tài)中完成,這明顯不是一個(gè)好選擇,所以可以讓后面那個(gè)線程“稍等一下”,但是不放棄cpu時(shí)間,請注意,自旋鎖是占用cpu時(shí)間的,只是減少了線程狀態(tài)切換的消耗,如果說一直在那等肯定會極大浪費(fèi)cpu性能,所以自旋次數(shù)試試10次,可以使用-XX:PreBlockSpin來更改。
- 自適應(yīng)性自旋鎖:jdk1.6引入自適應(yīng)性自旋鎖,意味著自旋時(shí)間不再固定,而是由前一次在同一個(gè)鎖上的自旋時(shí)間及鎖的擁有者的轉(zhuǎn)態(tài)來決定,例如線程如果自旋成功了,那么下次自旋的次數(shù)會增多,因?yàn)镴VM認(rèn)為既然上次成功了,那么這次自旋也很有可能成功,那么它會允許自旋的次數(shù)更多。反之,如果對于某個(gè)鎖,自旋很少成功,那么在以后獲取這個(gè)鎖的時(shí)候,自旋的次數(shù)會變少甚至忽略,避免浪費(fèi)處理器資源。有了自適應(yīng)性自旋,隨著程序運(yùn)行和性能監(jiān)控信息的不斷完善,JVM對程序鎖的狀況預(yù)測就會變得越來越準(zhǔn)確,JVM也就變得越來越聰明。
鎖消除
鎖消除是指虛擬機(jī)即時(shí)編譯器在運(yùn)行時(shí),對一些代碼上要求同步,但是被檢測到不可能存在共享數(shù)據(jù)競爭的鎖進(jìn)行消除。
在《深入理解java虛擬機(jī)》一文中這樣舉得例子:
public String concatString(String str1,String str2,String str3){
return str1+str2+str3;
}
String是一個(gè)不可變的類,在jdk1.5之前,這段代碼會轉(zhuǎn)為stringBuffer對象的連續(xù)append(),在jdk1.5之后,會轉(zhuǎn)為StringBuilder對象的連續(xù)append()操作,因?yàn)閟tringBuffer.append()都有一個(gè)同步代碼塊,而這段代碼很明顯并不會出現(xiàn)并發(fā)問題,所以雖然這里有鎖,但是在即時(shí)編譯后會被安全消除掉。
鎖粗化:
如果一些列的連續(xù)操作都是對同一個(gè)對象反復(fù)加鎖和解鎖,即時(shí)沒有沒有線程競爭,頻繁的進(jìn)行互斥同步操作也會導(dǎo)致不必要的性能損耗。
public String concatString2(String str1,String str2,String str3){
StringBuffer sb = new StringBuffer();
sb.append(str1);
sb.append(str2);
sb.append(str3);
return sb.toString();
}
前面說過stringBuffer.append()都有一個(gè)同步代碼塊,這種情況下如果虛擬機(jī)檢測到有這樣一串零碎的操作都是對用一個(gè)對象加鎖,將會把加鎖同步的范圍拓展(粗化)到整個(gè)操作序列的外部,也就是上面這段代碼只加鎖一次。
輕量級鎖
加鎖過程:
-
代碼進(jìn)入同步塊的時(shí)候,如果同步對象鎖狀態(tài)為無鎖狀態(tài)(鎖標(biāo)志位為"01"狀態(tài),是否為偏向鎖為"0"),虛擬機(jī)首先將在當(dāng)前線程的棧幀中建立一個(gè)名為鎖記錄(Lock Recored)的空間,用于儲存鎖對象目前的Mark Word的拷貝(官方把這份拷貝加了個(gè)Displaced前綴,即Displaced Mark Word)。此時(shí)線程堆棧和對象頭的狀態(tài)如下:
輕量級鎖cas操作前堆棧和對象的狀態(tài) 將對象頭的Mark Word拷貝到線程的鎖記錄(Lock Recored)中。
-
虛擬機(jī)將使用CAS操作嘗試將對象的Mark Word更新為指向Lock Record的指針。如果這個(gè)更新成功了,這個(gè)線程就擁有了該對象的鎖,并且對象Mark Word的鎖標(biāo)志位將轉(zhuǎn)變?yōu)?00",即表示此對象處于輕量級鎖的狀態(tài)。這時(shí)線程堆棧和對象頭的狀態(tài)如下:
輕量級鎖cas操作后堆棧和對象的狀態(tài) 如果更新失敗,虛擬機(jī)首先會檢查對象的Mark Word是否指向當(dāng)前線程的棧幀,如果是就說明當(dāng)前線程已經(jīng)擁有了這個(gè)對象的鎖,可以直接進(jìn)入同步塊繼續(xù)執(zhí)行,否則說明這個(gè)鎖對象已經(jīng)被其其它線程搶占了。如果多個(gè)線程競爭鎖,進(jìn)入自旋執(zhí)行上一步,自旋結(jié)束后仍未獲得鎖,輕量級鎖就需要膨脹為重量級鎖,鎖標(biāo)志位狀態(tài)值變?yōu)?10",Mark Word中儲存就是指向重量級的指針,當(dāng)前線程以及后面等待鎖的線程也要進(jìn)入阻塞狀態(tài)。
釋放鎖的過程:
- 使用CAS操作將對象當(dāng)前的Mark Word和線程中復(fù)制的Displaced Mark Word替換回來(依據(jù)Mark Word中鎖記錄指針是否還指向本線程的鎖記錄)。
- 如果替換成功,整個(gè)同步過程就完成了,恢復(fù)到無鎖的狀態(tài)(01)。
- 如果替換失敗,說明有其他線程嘗試獲取該鎖(此時(shí)鎖已膨脹),那就要在釋放鎖的同時(shí),喚醒被掛起的線程。
偏向鎖
目的是消除數(shù)據(jù)在無競爭情況下的同步原語,進(jìn)一步提高程序的運(yùn)行性能。如果說輕量級鎖是在無競爭的情況下使用CAS操作區(qū)消除同步使用的互斥量,那么偏向鎖就是在無競爭的情況下把整個(gè)同步都消除掉,連CAS操作都不用做了。偏向鎖默認(rèn)是開啟的,也可以關(guān)閉。
偏向鎖"偏",就是"偏心"的"偏",它的意思是這個(gè)鎖會偏向于第一個(gè)獲得它的程序,如果在接下來的執(zhí)行過程中,該鎖沒有被其他的線程獲取,則持有偏向鎖的線程將永遠(yuǎn)不需要再進(jìn)行同步。
獲取鎖的過程:
- 當(dāng)鎖第一次被線程嘗試獲取的時(shí)候,虛擬機(jī)會把對象頭中的標(biāo)志位設(shè)為"01",也就是偏向模式,同時(shí)使用cas操作吧獲取到這個(gè)鎖的線程id記錄在對象的mark word中,如果cas設(shè)置成功,持有偏向鎖的線程以后每次進(jìn)入這個(gè)鎖相關(guān)的同步塊時(shí)都不需要進(jìn)行任何同步操作。
-
當(dāng)有另外一個(gè)線程嘗試獲取這個(gè)鎖時(shí),偏向模式就結(jié)束了,檢查Mark Word是否為可偏向鎖的狀態(tài),即是否偏向鎖即為1即表示支持可偏向鎖,否則為0表示不支持可偏向鎖。如果是可偏向鎖,則檢查Mark Word儲存的線程ID是否為當(dāng)前線程ID,如果是則執(zhí)行同步塊,否則通過CAS操作去修改線程ID修改成本線程的ID,如果修改成功則執(zhí)行同步代碼塊,否則掛起這個(gè)線程,升級為輕量級鎖。
也就是根據(jù)鎖對象是否處于被鎖定的狀態(tài),撤銷偏向后到未鎖定(標(biāo)志位為01)或輕量級鎖(標(biāo)志位為00)的狀態(tài)。
偏向鎖和輕量級鎖的狀態(tài)轉(zhuǎn)化和對象Mark Word的關(guān)系如下:
偏向鎖和輕量級鎖的狀態(tài)轉(zhuǎn)化和對象Mark Word
鎖釋放
- 1:有其他線程來獲取這個(gè)鎖,偏向鎖的釋放采用了一種只有競爭才會釋放鎖的機(jī)制,線程是不會主動去釋放偏向鎖,需要等待其他線程來競爭。
-:2:等待全局安全點(diǎn)(在這個(gè)是時(shí)間點(diǎn)上沒有字節(jié)碼正在執(zhí)行)。
-:3:暫停擁有偏向鎖的線程,檢查持有偏向鎖的線程是否活著,如果不處于活動狀態(tài),則將對象頭設(shè)置為無鎖狀態(tài),否則設(shè)置為被鎖定狀態(tài)。如果鎖對象處于無鎖狀態(tài),則恢復(fù)到無鎖狀態(tài)(01),以允許其他線程競爭,如果鎖對象處于鎖定狀態(tài),則掛起持有偏向鎖的線程,并將對象頭Mark Word的鎖記錄指針改成當(dāng)前線程的鎖記錄,鎖升級為輕量級鎖狀態(tài)(00)。
重量級鎖
Synchronized是通過對象內(nèi)部的一個(gè)叫做 監(jiān)視器鎖(Monitor)來實(shí)現(xiàn)的。但是監(jiān)視器鎖本質(zhì)又是依賴于底層的操作系統(tǒng)的Mutex Lock來實(shí)現(xiàn)的。而操作系統(tǒng)實(shí)現(xiàn)線程之間的切換這就需要從用戶態(tài)轉(zhuǎn)換到核心態(tài),這個(gè)成本非常高,狀態(tài)之間的轉(zhuǎn)換需要相對比較長的時(shí)間,這就是為什么Synchronized效率低的原因。因此,這種依賴于操作系統(tǒng)Mutex Lock所實(shí)現(xiàn)的鎖我們稱之為 “重量級鎖”。




