synchronized的實(shí)現(xiàn)原理
synchronized的實(shí)現(xiàn)原理
大家好我是debug沒有bug的迪吧哥,今天不加班,和大家來扯一扯synchronized。
Synchronized通常稱為重量級鎖,但是Java SE1.6后對synchronized進(jìn)行了各種優(yōu)化。Java中每個(gè)對象都可作為鎖,具體有如下3種形式:
對于普通方法,鎖是當(dāng)前實(shí)例對象。
現(xiàn)象:先逐行輸出mythread1,線程1:RUNNABLE,線程2:BLOCKED,之后暫停五分鐘,逐行輸出sleep end,mythread2
分析:線程2與線程1擁有相同的鎖,線程1優(yōu)先級高于線程2,線程1優(yōu)先執(zhí)行,獲取到鎖,執(zhí)行sleep,未釋放鎖,線程2未獲取到鎖,處于阻塞狀態(tài),線程1sleep結(jié)束,輸出sleep end,并釋放鎖,線程2獲取到鎖,執(zhí)行輸出mythread2
結(jié)論:當(dāng)synchronized修飾普通方法時(shí),鎖對象即為當(dāng)前對象
對于靜態(tài)同步方法,鎖的是當(dāng)前類的class對象。
對上述代碼稍作修改,在dosome方法上添加static關(guān)鍵字,main方法中,線程2對象初始化時(shí),傳入MyThread1.class
現(xiàn)象:與上述現(xiàn)象一致
分析:同上
結(jié)論:當(dāng)synchronized修飾靜態(tài)方法時(shí),鎖對象為當(dāng)前Class對象
對于同步方法快,鎖的是synchonized括號(hào)內(nèi)的配置對象。
當(dāng)一個(gè)線程試圖訪問同步代碼塊時(shí),首先必須得到鎖,退出或拋出異常必須釋放鎖。鎖存在哪里? 鎖有哪些信息?
JVM規(guī)范可看到Synchonized在JVM里的實(shí)現(xiàn)原理,JVM基于進(jìn)入與退出Monitor對象實(shí)現(xiàn)方法同步與代碼塊同步。代碼塊同步使用monitorenter和monitorexit指令實(shí)現(xiàn)的,方法同步用另外方式實(shí)現(xiàn),細(xì)節(jié)再JVM規(guī)范沒詳細(xì)說明。方法同步用這兩個(gè)指令實(shí)現(xiàn)。
monitorenter指令在編譯后插入到同步代碼塊開始位置,而monitorexit插入到方法結(jié)束與異常處,JVM保證每個(gè)monitorenter必須有相應(yīng)monitorexit與之配對。任何對象有一個(gè)monitor與之關(guān)聯(lián),當(dāng)一個(gè)monitor被持有后,它處于鎖定狀態(tài)。
我們執(zhí)行以下代碼,并且結(jié)合hsdis-amd64.dl工具查看控制臺(tái)的字節(jié)碼指令:
字節(jié)碼指令:
我們可以看到 有ACC_SYNCHRONIZED, 它使用了monitorenter與monitorexit指令,隱式執(zhí)行了Lock與Unlock操作,提供原子性。
synchronized鎖的原理
jdk1.6后對Synchonized鎖優(yōu)化,有偏向鎖、輕量級鎖、重量級鎖。
我們通過一下三個(gè)問題來了解synchronized原理:
synchronized如何實(shí)現(xiàn)鎖
為什么任何一個(gè)對象都可以成為鎖
鎖存在哪個(gè)地方?
了解synchronized前,我們需了解兩個(gè)重要概念,一個(gè)是對象頭,一個(gè)是monitor。
Java對象頭:
在Hotspot虛擬機(jī)中,對象在內(nèi)存的布局分為三塊區(qū)域:對象頭、實(shí)例數(shù)據(jù)與對象填充;Java對象頭是實(shí)現(xiàn)synchronized鎖對象基礎(chǔ),synchronized使用鎖對象存儲(chǔ)在Java對象頭里。是輕量級鎖與偏向鎖關(guān)鍵。
Mawrk Word:
Mark word用于存儲(chǔ)對象自身運(yùn)行時(shí)數(shù)據(jù),如哈希瑪、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有鎖、偏向線程ID\偏向時(shí)間戳等。Java對象頭一般占有兩機(jī)器碼,如下所示:
Monitor:
Monitor是什么? 可理解為一個(gè)同步工具,也是一種同步機(jī)制。Java對象是天生的Monitor,每個(gè)object對象里markOop->monitor()里可保存ObjectMonitor對象。
Synchronized如何實(shí)現(xiàn)鎖?
了解了對象頭和monitor之后我們可以分析synchronized鎖的實(shí)現(xiàn)。
樂觀鎖:
樂觀鎖認(rèn)為讀多寫少,每次拿數(shù)據(jù)的時(shí)候認(rèn)為別人不修改,不會(huì)上鎖,更新的時(shí)候判斷這個(gè)期間又沒人更新數(shù)據(jù),寫的時(shí)候先讀出版本號(hào),然后加鎖操作,如果失敗,重復(fù)操作。java中樂觀鎖基本通過CAS實(shí)現(xiàn),比較當(dāng)前值和傳入值是否一樣。
悲觀鎖:
悲觀就是認(rèn)為寫多讀少,拿數(shù)據(jù)時(shí)都認(rèn)為別人會(huì)修改,所以每次讀寫會(huì)上鎖,所以別人讀寫數(shù)據(jù)會(huì)block直到拿到鎖。java中悲觀鎖是Synchronized,AQS框架的鎖先嘗試cas樂觀鎖獲取鎖,獲取不到轉(zhuǎn)化為悲觀鎖,如RetreenLock,還有數(shù)據(jù)庫一般本身鎖的機(jī)制也是基于悲觀鎖的機(jī)制實(shí)現(xiàn)的。
自旋鎖(CAS):
自旋鎖讓不滿足條件的線程等待一段時(shí)間,自旋就是一段循環(huán),通過占用處理器時(shí)間避免線程切換帶來的開銷,但是如果持有鎖定線程不能很快釋放鎖會(huì)浪費(fèi)資源,下面是自旋鎖代碼演示。
偏向鎖:
很多情況,鎖沒有存在多線程競爭,而是由同一線程多次獲得,為了讓線程獲取鎖代價(jià)變低引入偏向鎖。下圖是偏向鎖獲得與撤銷圖。
1、線程訪問同步塊獲取鎖,對想頭與棧幀鎖記錄存儲(chǔ)鎖偏向的線程ID。
2、線程進(jìn)入退出同步快不要cas操作,只需簡單測試Mark Word是否存儲(chǔ)線程偏向鎖。
3、測試成功,表示線程獲得鎖,測試失敗,測試Mark Word偏向鎖標(biāo)識(shí)是否為01,沒有設(shè)置則使用CAS競爭鎖,設(shè)置了。嘗試用CAS把對象頭偏向鎖指向當(dāng)前線程。
4、執(zhí)行同步塊,線程2訪問同步塊,檢查對象頭Mark Word是否存儲(chǔ)線程2的偏向鎖,不是側(cè)進(jìn)入cas替換,此時(shí)替換失敗,線程1已經(jīng)替換。
輕量級鎖:
引入輕量級鎖的主要目的是在多沒有多線程競爭的前提下,減少傳統(tǒng)的重量級鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能消耗。當(dāng)關(guān)閉偏向鎖功能或者多個(gè)線程競爭偏向鎖導(dǎo)致偏向鎖升級為輕量級鎖,則會(huì)嘗試獲取輕量級鎖,下面是輕量級鎖的流程圖:
重量級鎖:
重量級鎖通過對象內(nèi)部的監(jiān)視器(monitor)實(shí)現(xiàn),其中monitor的本質(zhì)是依賴于底層操作系統(tǒng)的Mutex Lock實(shí)現(xiàn),操作系統(tǒng)實(shí)現(xiàn)線程之間的切換需要從用戶態(tài)到內(nèi)核態(tài)的切換,切換成本非常高。主要是,當(dāng)系統(tǒng)檢查到鎖是重量級鎖之后,會(huì)把等待想要獲得鎖的線程進(jìn)行阻塞,被阻塞的線程不會(huì)消耗cup。但是阻塞或者喚醒一個(gè)線程時(shí),都需要操作系統(tǒng)來幫忙,這就需要從用戶態(tài)轉(zhuǎn)換到內(nèi)核態(tài),而轉(zhuǎn)換狀態(tài)是需要消耗很多時(shí)間的,有可能比用戶執(zhí)行代碼的時(shí)間還要長。這就是說為什么重量級線程開銷很大的。
好了,今天的分享就到這里了,我是debug沒bug的迪吧哥,你的點(diǎn)贊關(guān)注是對我的最大支持,感謝你的分享。
公眾號(hào)、b站、知乎、csdn:迪巴哥沒八哥