synchronized實現(xiàn)原理
在java代碼中使用synchronized可是使用在代碼塊和方法中锰茉,根據(jù)Synchronized用的位置可以有這些使用場景:
如果鎖的是類的話呢蔫,盡管new多個實例對象,但他們?nèi)匀皇菍儆谕粋€類依然會被鎖住,即線程之間保證同步關(guān)系片吊。
對象鎖(monitor)機制
-
鎖住的是類
用javap -v SynchronizedDemo.class查看字節(jié)碼文件:
image.png
- 上面用黃色高亮的部分就是需要注意的部分了绽昏,這也是添Synchronized關(guān)鍵字之后獨有的。執(zhí)行同步代碼塊后首先要先執(zhí)行monitorenter指令俏脊,退出的時候monitorexit指令
- 使用Synchronized進(jìn)行同步全谤,其關(guān)鍵就是必須要對對象的監(jiān)視器monitor進(jìn)行獲取,當(dāng)線程獲取monitor后才能繼續(xù)往下執(zhí)行爷贫,否則就只能等待认然。而這個獲取的過程是互斥的,即同一時刻只有一個線程能夠獲取到monitor
- 執(zhí)行靜態(tài)同步方法的時候就只有一條monitorexit指令漫萄,并沒有monitorenter獲取鎖的指令卷员。這就是鎖的重入性,即在同一鎖程中腾务,線程不需要再次獲取同一把鎖毕骡。Synchronized先天具有重入性。每個對象擁有一個計數(shù)器岩瘦,當(dāng)線程獲取該對象鎖后未巫,計數(shù)器就會加一,釋放鎖后就會將計數(shù)器減一启昧。
- 任意一個對象都擁有自己的監(jiān)視器叙凡,當(dāng)這個對象由同步塊或者這個對象的同步方法調(diào)用時,執(zhí)行方法的線程必須先獲取該對象的監(jiān)視器才能進(jìn)入同步塊和同步方法箫津,如果沒有獲取到監(jiān)視器的線程將會被阻塞在同步塊和同步方法的入口處狭姨,進(jìn)入到BLOCKED狀態(tài)
synchronized的happens-before關(guān)系
對同一個監(jiān)視器的解鎖宰啦,happens-before于對該監(jiān)視器的加鎖苏遥。繼續(xù)來看代碼:
public class MonitorDemo {
private int a = 0;
public synchronized void writer() { // 1
a++; // 2
} // 3
public synchronized void reader() { // 4
int i = a; // 5
} // 6
}
根據(jù)happens-before的定義中的一條:如果A happens-before B,則A的執(zhí)行結(jié)果對B可見赡模,并且A的執(zhí)行順序先于B田炭。線程A先對共享變量A進(jìn)行加一,由2 happens-before 5關(guān)系可知線程A的執(zhí)行結(jié)果對線程B可見即線程B所讀取到的a的值為1漓柑。
第四步獲得鎖之后教硫,writer方法里所有的各種變量的處理,對于reader方法辆布,都是可見的瞬矩。
鎖獲取和鎖釋放的內(nèi)存語義
從整體上來看,線程A的執(zhí)行結(jié)果(a=1)對線程B是可見的锋玲,實現(xiàn)原理為:釋放鎖的時候會將值刷新到主內(nèi)存中景用,其他線程獲取鎖時會強制從主內(nèi)存中獲取最新的值。另外也驗證了2 happens-before 5惭蹂,2的執(zhí)行結(jié)果對5是可見的伞插。
從橫向來看割粮,這就像線程A通過主內(nèi)存中的共享變量和線程B進(jìn)行通信,A 告訴 B 我們倆的共享數(shù)據(jù)現(xiàn)在為1啦媚污,這種線程間的通信機制正好吻合java的內(nèi)存模型正好是共享內(nèi)存的并發(fā)模型結(jié)構(gòu)舀瓢。
synchronized優(yōu)化
CAS的應(yīng)用場景
- 在J.U.C包中利用CAS實現(xiàn)類有很多,可以說是支撐起整個concurrency包的實現(xiàn)耗美,在Lock實現(xiàn)中會有CAS改變state變量京髓,在atomic包中的實現(xiàn)類也幾乎都是用CAS實現(xiàn),關(guān)于這些具體的實現(xiàn)場景在之后會詳細(xì)聊聊幽歼,現(xiàn)在有個印象就好了
- CAS的問題
- ABA問題
因為CAS會檢查舊值有沒有變化朵锣,這里存在這樣一個有意思的問題。比如一個舊值A(chǔ)變?yōu)榱顺葿甸私,然后再變成A诚些,剛好在做CAS時檢查發(fā)現(xiàn)舊值并沒有變化依然為A,但是實際上的確發(fā)生了變化皇型。解決方案可以沿襲數(shù)據(jù)庫中常用的樂觀鎖方式诬烹,添加一個版本號可以解決。原來的變化路徑A->B->A就變成了1A->2B->3C弃鸦。java這么優(yōu)秀的語言绞吁,當(dāng)然在java 1.5后的atomic包中提供了AtomicStampedReference來解決ABA問題,解決思路就是這樣的唬格。
- ABA問題
- 自旋時間過長
使用CAS時非阻塞同步家破,也就是說不會將線程掛起,會自旋(無非就是一個死循環(huán))進(jìn)行下一次嘗試购岗,如果這里自旋時間過長對性能是很大的消耗汰聋。如果JVM能支持處理器提供的pause指令,那么在效率上會有一定的提升喊积。
- 自旋時間過長
- 只能保證一個共享變量的原子操作
當(dāng)對一個共享變量執(zhí)行操作時CAS能保證其原子性烹困,如果對多個共享變量進(jìn)行操作,CAS就不能保證其原子性。有一個解決方案是利用對象整合多個共享變量乾吻,即一個類中的成員變量就是這幾個共享變量髓梅。然后將這個對象做CAS操作就可以保證其原子性。atomic中提供了AtomicReference來保證引用對象之間的原子性绎签。
- 只能保證一個共享變量的原子操作
Java對象頭
在同步的時候是獲取對象的monitor,即獲取到對象的鎖枯饿。那么對象的鎖怎么理解?無非就是類似對對象的一個標(biāo)志诡必,那么這個標(biāo)志就是存放在Java對象的對象頭奢方。Java對象頭里的Mark Word里默認(rèn)的存放的對象的Hashcode,分代年齡和鎖標(biāo)記位。32為JVM Mark Word默認(rèn)存儲結(jié)構(gòu)為
Java SE 1.6中,鎖一共有4種狀態(tài)袱巨,級別從低到高依次是:無鎖狀態(tài)阁谆、偏向鎖狀態(tài)、輕量級鎖狀態(tài)和重量級鎖狀態(tài)愉老,這幾個狀態(tài)會隨著競爭情況逐漸升級场绿。鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖后不能降級成偏向鎖嫉入。這種鎖升級卻不能降級的策略焰盗,目的是為了提高獲得鎖和釋放鎖的效率
偏向鎖
HotSpot的作者經(jīng)過研究發(fā)現(xiàn),大多數(shù)情況下咒林,鎖不僅不存在多線程競爭熬拒,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低而引入了偏向鎖垫竞。當(dāng)一個線程訪問同步塊并獲取鎖時澎粟,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID,以后該線程在進(jìn)入和退出同步塊時不需要進(jìn)行CAS操作來加鎖和解鎖欢瞪,只需簡單地測試一下對象頭的Mark Word里是否存儲著指向當(dāng)前線程的偏向鎖活烙。如果測試成功,表示線程已經(jīng)獲得了鎖遣鼓。如果測試失敗啸盏,則需要再測試一下Mark Word中偏向鎖的標(biāo)識是否設(shè)置成1(表示當(dāng)前是偏向鎖):如果沒有設(shè)置,則使用CAS競爭鎖骑祟;如果設(shè)置了回懦,則嘗試使用CAS將對象頭的偏向鎖指向當(dāng)前線程
偏向鎖使用了一種等到競爭出現(xiàn)才釋放鎖的機制,所以當(dāng)其他線程嘗試競爭偏向鎖時次企,持有偏向鎖的線程才會釋放鎖怯晕。
偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有正在執(zhí)行的字節(jié)碼)抒巢。它會首先暫停擁有偏向鎖的線程贫贝,然后檢查持有偏向鎖的線程是否活著秉犹,如果線程不處于活動狀態(tài)蛉谜,則將對象頭設(shè)置成無鎖狀態(tài);如果線程仍然活著崇堵,擁有偏向鎖的棧會被執(zhí)行型诚,遍歷偏向?qū)ο蟮逆i記錄,棧中的鎖記錄和對象頭的Mark Word要么重新偏向于其他線程鸳劳,要么恢復(fù)到無鎖或者標(biāo)記對象不適合作為偏向鎖狰贯,最后喚醒暫停的線程。
偏向鎖在Java 6和Java 7里是默認(rèn)啟用的,但是它在應(yīng)用程序啟動幾秒鐘之后才激活涵紊,如有必要可以使用JVM參數(shù)來關(guān)閉延遲:-XX:BiasedLockingStartupDelay=0傍妒。如果你確定應(yīng)用程序里所有的鎖通常情況下處于競爭狀態(tài),可以通過JVM參數(shù)關(guān)閉偏向鎖:-XX:-UseBiasedLocking=false摸柄,那么程序默認(rèn)會進(jìn)入輕量級鎖狀態(tài)
輕量級鎖
線程在執(zhí)行同步塊之前颤练,JVM會先在當(dāng)前線程的棧楨中創(chuàng)建用于存儲鎖記錄的空間,并將對象頭中的Mark Word復(fù)制到鎖記錄中驱负,官方稱為Displaced Mark Word嗦玖。然后線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功跃脊,當(dāng)前線程獲得鎖宇挫,如果失敗,表示其他線程競爭鎖酪术,當(dāng)前線程便嘗試使用自旋來獲取鎖器瘪。
輕量級解鎖時,會使用原子的CAS操作將Displaced Mark Word替換回到對象頭绘雁,如果成功娱局,則表示沒有競爭發(fā)生。如果失敗咧七,表示當(dāng)前鎖存在競爭衰齐,鎖就會膨脹成重量級鎖。
-
各種鎖的比較
image.png