在多線程并發(fā)編程中 synchronized 一直是元老級角色,很多人都會稱呼它為重量級鎖收苏。但是漓柑,隨著 Java SE 1.6 對synchronized 進(jìn)行了各種優(yōu)化之后驰吓,有些情況下它就并不那么重凳兵,Java SE 1.6 中為了減少獲得鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖
鎖的存儲
JVM 源碼實現(xiàn)
當(dāng)我們在 Java 代碼中飞蹂,使用 new 創(chuàng)建一個對象實例的時候薇缅,(hotspot 虛擬機)JVM 層面實際上會創(chuàng)建一個instanceOopDesc 對象。
Hotspot 虛擬機采用 OOP-Klass 模型來描述 Java 對象實例饭望,OOP(Ordinary Object Point)指的是普通對象指針塑娇,Klass 用來描述對象實例的具體類型芥永。Hotspot 采用instanceOopDesc 和 arrayOopDesc 來 描述對象 頭,arrayOopDesc 對象用來描述數(shù)組類型instanceOopDesc 的定義在 Hotspot 源 碼 中 的instanceOop.hpp 文件中钝吮,另外,arrayOopDesc 的定義對應(yīng) arrayOop.hpp
從 instanceOopDesc 代碼中可以看到 instanceOopDesc繼承自 oopDesc,oopDesc 的定義載 Hotspot 源碼中的oop.hpp 文件中,在普通實例對象中奇瘦,oopDesc 的定義包含兩個成員棘催,分別是 _mark 和 _metadata_mark 表示對象標(biāo)記、屬于 markOop 類型耳标,也就是Mark World醇坝,它記錄了對象和鎖有關(guān)的信息_metadata 表示類元信息,類元信息存儲的是對象指向它的類元數(shù)據(jù)(Klass)的首地址次坡,其中 Klass 表示普通指針呼猪、_compressed_klass 表示壓縮類指針
Mark World
在 Hotspot 中,markOop 的定義在 markOop.hpp 中
Mark word 記錄了對象和鎖有關(guān)的信息砸琅,當(dāng)某個對象被synchronized 關(guān)鍵字當(dāng)成同步鎖時宋距,那么圍繞這個鎖的一系列操作都和 Mark word 有關(guān)系。Mark Word 在 32 位虛擬機的長度是 32bit症脂、在 64 位虛擬機的長度是 64bit谚赎,Mark Word 里面存儲的數(shù)據(jù)會隨著鎖標(biāo)志位的變化而變化。
Java 對象頭
鎖存在 Java 對象頭里诱篷。如果對象是數(shù)組類型壶唤,則虛擬機用 3 個 Word(字寬)存儲對象頭,如果對象是非數(shù)組類型棕所,則用 2 字寬存儲對象頭闸盔。在 32 位虛擬機中,一字寬等于四字節(jié)琳省,即 32bit迎吵。
Java 對象頭里的 Mark Word 里默認(rèn)存儲對象的 HashCode,分代年齡和鎖標(biāo)記位岛啸。32 位 JVM 的 Mark Word 的默認(rèn)存儲結(jié)構(gòu)如下:
在運行期間 Mark Word 里存儲的數(shù)據(jù)會隨著鎖標(biāo)志位的變化而變化钓觉。Mark Word 可能變化為存儲以下 4 種數(shù)據(jù):
在 64 位虛擬機下,Mark Word 是 64bit 大小的坚踩,其存儲結(jié)構(gòu)如下:
為什么任何對象都可以實現(xiàn)鎖
- 首先荡灾,Java 中的每個對象都派生自 Object 類,而每個Java Object 在 JVM 內(nèi)部都有一個 native 的 C++對象oop/oopDesc 進(jìn)行對應(yīng)瞬铸。
-
線程在獲取鎖的時候批幌,實際上就是獲得一個監(jiān)視器對象(monitor) ,monitor 可以認(rèn)為是一個同步對象,所有的Java 對象是天生攜帶 monitor嗓节。在 hotspot 源碼的markOop.hpp 文件中荧缘,可以看到下面這段代碼。
image.png
多個線程訪問同步代碼塊時拦宣,相當(dāng)于去爭搶對象監(jiān)視器修改對象中的鎖標(biāo)識,上面的代碼中ObjectMonitor這個對象和線程爭搶鎖的邏輯有密切的關(guān)系
鎖升級
Java SE1.6 為了減少獲得鎖和釋放鎖所帶來的性能消耗截粗,引入了“偏向鎖”和“輕量級鎖”信姓,所以在 Java SE1.6 里鎖一共有四種狀態(tài),無鎖狀態(tài)绸罗,偏向鎖狀態(tài)意推,輕量級鎖狀態(tài)和重量級鎖狀態(tài),它會隨著競爭情況逐漸升級珊蟀。鎖可以升級但不能降級菊值,意味著偏向鎖升級成輕量級鎖后不能降級成偏向鎖。這種鎖升級卻不能降級的策略育灸,目的是為了提高獲得鎖和釋放鎖的效率腻窒。
偏向鎖
當(dāng)一個線程訪問加了同步鎖的代碼塊時,會在對象頭中存儲當(dāng)前線程的 ID磅崭,后續(xù)這個線程進(jìn)入和退出這段加了同步鎖的代碼塊時儿子,不需要再次加鎖和釋放鎖。而是直接比較對象頭里面是否存儲了指向當(dāng)前線程的偏向鎖绽诚。如果相等表示偏向鎖是偏向于當(dāng)前線程的典徊,就不需要再嘗試獲得鎖了
-
獲取偏向鎖
- 首先獲取鎖 對象的 Markword,判斷是否處于可偏向狀態(tài)恩够。(biased_lock=1卒落、且 ThreadId 為空)
- 如果是可偏向狀態(tài),則通過 CAS 操作蜂桶,把當(dāng)前線程的 ID寫入到 MarkWord
a) 如果 cas 成功儡毕,那么 markword 就會變成這樣。表示已經(jīng)獲得了鎖對象的偏向鎖扑媚,接著執(zhí) 行同步代碼塊
b) 如果 cas 失敗腰湾,說明有其他線程已經(jīng)獲得了偏向鎖,這種情況說明當(dāng)前鎖存在競爭疆股,需要撤銷已獲得偏向鎖的線程费坊,并且把它持有的鎖升級為輕量級鎖(這個操作需要等到全局安全點,也就是沒有線程在執(zhí)行字節(jié)碼)才能執(zhí)行 - 如果是已偏向狀態(tài)旬痹,需要檢查 markword 中存儲的ThreadID 是否等于當(dāng)前線程的 ThreadID
a) 如果相等附井,不需要再次獲得鎖,可直接執(zhí)行同步代碼塊
b) 如果不相等两残,說明當(dāng)前鎖偏向于其他線程永毅,需要撤銷偏向鎖并升級到輕量級鎖
-
撤銷偏向鎖
偏向鎖的撤銷并不是把對象恢復(fù)到無鎖可偏向狀態(tài)(因為偏向鎖并不存在鎖釋放的概念),而是在獲取偏向鎖的過程中人弓,發(fā)現(xiàn) cas 失敗也就是存在線程競爭時沼死,直接把被偏向的鎖對象升級到被加了輕量級鎖的狀態(tài)。對原持有偏向鎖的線程進(jìn)行撤銷時崔赌,原獲得偏向鎖的線程有兩種情況:
- 原獲得偏向鎖的線程如果已經(jīng)退出了臨界區(qū)意蛀,也就是同步代碼塊執(zhí)行完了耸别,那么這個時候會把對象頭設(shè)置成無鎖狀態(tài)并且爭搶鎖的線程可以基于 CAS 重新偏向當(dāng)前線程。
- 如果原獲得偏向鎖的線程的同步代碼塊還沒執(zhí)行完浸间,處于臨界區(qū)之內(nèi)太雨,這個時候會把原獲得偏向鎖的線程升級為輕量級鎖后繼續(xù)執(zhí)行同步代碼塊。
在我們的應(yīng)用開發(fā)中魁蒜,絕大部分情況下一定會存在 2 個以上的線程競爭,那么如果開啟偏向鎖吩翻,反而會提升獲取鎖的資源消耗兜看。所以可以通過 jvm 參數(shù)UseBiasedLocking 來設(shè)置開啟或關(guān)閉偏向鎖。
偏向鎖的流程圖分析
輕量級鎖
- 輕量級鎖的加鎖
鎖升級為輕量級鎖之后狭瞎,對象的 Markword 也會進(jìn)行相應(yīng)的的變化细移。升級為輕量級鎖的過程:- 線程在自己的棧楨中創(chuàng)建鎖記錄 LockRecord。
- 將鎖對象的對象頭中的MarkWord復(fù)制到線程的剛剛創(chuàng)建的鎖記錄中熊锭。
- 將鎖記錄中的 Owner 指針指向鎖對象弧轧。
-
將鎖對象的對象頭的 MarkWord替換為指向鎖記錄的指針。
image.png
image.png
- 自旋鎖
輕量級鎖在加鎖過程中碗殷,用到了自旋鎖
所謂自旋精绎,就是指當(dāng)有另外一個線程來競爭鎖時,這個線程會在原地循環(huán)等待锌妻,而不是把該線程給阻塞代乃,直到那個獲得鎖的線程釋放鎖之后,這個線程就可以馬上獲得鎖的仿粹。注意搁吓,鎖在原地循環(huán)的時候,是會消耗 cpu 的吭历,就相當(dāng)于在執(zhí)行一個啥也沒有的 for 循環(huán)堕仔。所以,輕量級鎖適用于那些同步代碼塊執(zhí)行的很快的場景晌区,這樣摩骨,線程原地等待很短的時間就能夠獲得鎖了。
自旋鎖的使用契讲,其實也是有一定的概率背景仿吞,在大部分同步代碼塊執(zhí)行的時間都是很短的。所以通過看似無異議的循環(huán)反而能提升鎖的性能捡偏。但是自旋必須要有一定的條件控制唤冈,否則如果一個線程執(zhí)行同步代碼塊的時間很長,那么這個線程不斷的循環(huán)反而會消耗 CPU 資源银伟。默認(rèn)情況下自旋的次數(shù)是 10 次你虹,可以通過 preBlockSpin 來修改绘搞。
在 JDK1.6 之后,引入了自適應(yīng)自旋鎖傅物,自適應(yīng)意味著自旋的次數(shù)不是固定不變的夯辖,而是根據(jù)前一次在同一個鎖上自旋的時間以及鎖的擁有者的狀態(tài)來決定。
如果在同一個鎖對象上董饰,自旋等待剛剛成功獲得過鎖蒿褂,并且持有鎖的線程正在運行中,那么虛擬機就會認(rèn)為這次自旋也是很有可能再次成功卒暂,進(jìn)而它將允許自旋等待持續(xù)相對更長的時間啄栓。如果對于某個鎖,自旋很少成功獲得過也祠,那在以后嘗試獲取這個鎖時將可能省略掉自旋過程昙楚,直接阻塞線程,避免浪費處理器資源
- 輕量級鎖的解鎖
輕量級鎖的鎖釋放邏輯其實就是獲得鎖的逆向邏輯诈嘿,通過CAS 操作把線程棧幀中的 LockRecord 替換回到鎖對象的MarkWord 中堪旧,如果成功表示沒有競爭。如果失敗奖亚,表示當(dāng)前鎖存在競爭淳梦,那么輕量級鎖就會膨脹成為重量級鎖
輕量級鎖的流程圖分析
重量級鎖
當(dāng)輕量級鎖膨脹到重量級鎖之后,意味著線程只能被掛起阻塞來等待被喚醒了遂蛀。
重量鎖在JVM中又叫對象監(jiān)視器(Monitor)谭跨,它很像C中的Mutex,除了具備Mutex(0|1)互斥的功能李滴,它還負(fù)責(zé)實現(xiàn)了Semaphore(信號量)的功能螃宙,也就是說它至少包含一個競爭鎖的隊列,和一個信號阻塞隊列(wait隊列)所坯,前者負(fù)責(zé)做互斥谆扎,后一個用于做線程同步.
加了同步代碼塊以后,在字節(jié)碼中會看到一個monitorenter 和 monitorexit芹助。
每一個 JAVA 對象都會與一個監(jiān)視器 monitor 關(guān)聯(lián)堂湖,我們可以把它理解成為一把鎖,當(dāng)一個線程想要執(zhí)行一段被synchronized 修飾的同步方法或者代碼塊時状土,該線程得先獲取到 synchronized 修飾的對象對應(yīng)的 monitor无蜂。
monitorenter 表示去獲得一個對象監(jiān)視器。monitorexit 表示釋放 monitor 監(jiān)視器的所有權(quán)蒙谓,使得其他被阻塞的線程可以嘗試去獲得這個監(jiān)視器
monitor 依賴操作系統(tǒng)的 MutexLock(互斥鎖)來實現(xiàn)的, 線程被阻塞后便進(jìn)入內(nèi)核(Linux)調(diào)度狀態(tài)斥季,這個會導(dǎo)致系統(tǒng)在用戶態(tài)與內(nèi)核態(tài)之間來回切換,嚴(yán)重影響鎖的性能
——學(xué)自咕泡學(xué)院