在java中存在兩種鎖機制,分別是synchronized和Lock俗孝。下面我會總結(jié)一下synchronized的實現(xiàn)原理和涉及的一些鎖優(yōu)化機制。
synchronized的使用
synchronized 的作用:
- 確保線程互斥的訪問代碼塊,同一時刻只有一個方法可以進入到臨界區(qū)
- 保證共享變量的修改能及時可見
- 有效解決重排序問題
synchronized 使用方式:
- 修飾實例對象中的實例方法业踢,鎖的是當前實例對象(this)玻淑。
- 修飾靜態(tài)方法嗽冒,鎖的是當前類的class對象。
- 使用同步代碼塊岁忘,鎖的是括號里的對象
synchronized內(nèi)部實現(xiàn)原理
監(jiān)視器鎖
synchronized 同步代碼塊的語義底層是基于對象內(nèi)部的監(jiān)視器鎖(monitor)辛慰,分別是使用 monitorenter 和 monitorexit 指令完成。其實 wait/notify 也依賴于 monitor 對象干像,所以其一般要在 synchronized 同步的方法或代碼塊內(nèi)使用。monitorenter 指令在編譯為字節(jié)碼后插入到同步代碼塊的開始位置驰弄,monitorexit 指令在編譯為字節(jié)碼后插入到方法結(jié)束處和異常處麻汰。JVM 要保證每個 monitorenter 必須有對應的 moniorexit。
monitorenter:每個對象都有一個監(jiān)視器鎖(monitor)戚篙,當 monitor 被某個線程占用時就會處于鎖定狀態(tài)五鲫,線程執(zhí)行 monitorenter 指令時嘗試獲得 monitor 的所有權(quán),即嘗試獲取對象的鎖岔擂。過程如下:
- 如果 monitor 的進入數(shù)為0位喂,則該線程進入 monitor,然后將進入數(shù)設置為1乱灵,該線程即為 monitor 的所有者塑崖;
- 如果線程已經(jīng)占有monitor,只是重新進入痛倚,則monitor的進入數(shù)+1规婆;
- 如果其他線程已經(jīng)占用 monitor,則該線程處于阻塞狀態(tài)蝉稳,直至 monitor 的進入數(shù)為0抒蚜,再重新嘗試獲得 monitor 的所有權(quán)
monitorexit:執(zhí)行 monitorexit 的線程必須是 objectref 所對應的 monitor 的所有者。執(zhí)行指令時耘戚,monitor 的進入數(shù)減1嗡髓,如果減1后進入數(shù)為0,則線程退出 monitor收津,不再是這個 monitor 的所有者饿这,其他被這個 monitor 阻塞的線程可以嘗試獲取這個 monitor 的所有權(quán)。
線程狀態(tài)和狀態(tài)轉(zhuǎn)化
在 HotSpot JVM 中朋截,monitor 由 ObjectMonitor 實現(xiàn)蛹稍,其主要數(shù)據(jù)結(jié)構(gòu)如下:
<pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px;">ObjectMonitor() {
_header = NULL;
_count = 0; //記錄個數(shù)
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL; //持有monitor的線程
_WaitSet = NULL; //處于wait狀態(tài)的線程,會被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //處于等待鎖block狀態(tài)的線程部服,會被加入到該列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
</pre>
ObjectMonitor 中有兩個隊列唆姐,_WaitSet 和 _EntryList,用來保存 ObjectWaiter 對象列表(每個等待鎖的線程都會被封裝成 ObjectWaiter 對象)廓八,_owner 指向持有 ObjectMonitor 對象的線程奉芦。
- 當多個線程同時訪問一段同步代碼時赵抢,首先會進入 _EntryList,等待鎖處于阻塞狀態(tài)声功。
- 當線程獲取到對象的 monitor 后進入 The Owner 區(qū)域烦却,并把 ObjectMonitor 中的 _owner 變量設置為當前線程,同時 monitor 中的計數(shù)器 count 加1先巴。
- 若線程調(diào)用 wait() 方法其爵,將釋放當前持有的 monitor,_owner 變量恢復為 null伸蚯,count 減1摩渺,同時該線程進入 _WaitSet 集合中等待被喚醒,處于 waiting 狀態(tài)剂邮。
- 若當前線程執(zhí)行完畢摇幻,將釋放 monitor 并復位變量的值,以便其他線程進入獲取 monitor挥萌。
過程如下圖所示:
鎖優(yōu)化
在 JDK1.6 之后绰姻,出現(xiàn)了各種鎖優(yōu)化技術(shù),如輕量級鎖引瀑、偏向鎖狂芋、適應性自旋、鎖粗化伤疙、鎖消除等银酗,這些技術(shù)都是為了在線程間更高效的解決競爭問題,從而提升程序的執(zhí)行效率徒像。
通過引入輕量級鎖和偏向鎖來減少重量級鎖的使用黍特。鎖的狀態(tài)總共分四種:無鎖狀態(tài)、偏向鎖锯蛀、輕量級鎖和重量級鎖灭衷。鎖隨著競爭情況可以升級,但鎖升級后不能降級旁涤,意味著不能從輕量級鎖狀態(tài)降級為偏向鎖狀態(tài)翔曲,也不能從重量級鎖狀態(tài)降級為輕量級鎖狀態(tài)。
無鎖狀態(tài) → 偏向鎖狀態(tài) → 輕量級鎖 → 重量級鎖
對象頭
在JVM中劈愚,對象在內(nèi)存中的布局分為三塊區(qū)域:對象頭瞳遍、實例數(shù)據(jù)和對齊填充,頭對象菌羽,是實現(xiàn)synchronized鎖對象的基礎掠械。要理解輕量級鎖和偏向鎖的運行機制,還要從了解對象頭(Object Header)開始。對象頭分為兩部分:
1猾蒂、Mark Word:存儲對象自身的運行時數(shù)據(jù)均唉,如:Hash Code,GC 分代年齡肚菠、鎖信息舔箭。這部分數(shù)據(jù)在32位和64位的 JVM 中分別為 32bit 和 64bit∥梅辏考慮空間效率层扶,Mark Word 被設計為非固定的數(shù)據(jù)結(jié)構(gòu),以便在極小的空間內(nèi)存儲盡量多的信息时捌,32bit的 Mark Word 如下圖所示:
2怒医、存儲指向方法區(qū)對象類型數(shù)據(jù)的指針,如果是數(shù)組對象的話奢讨,額外會存儲數(shù)組的長度
重量級鎖
monitor 監(jiān)視器鎖本質(zhì)上是依賴操作系統(tǒng)的 Mutex Lock 互斥量 來實現(xiàn)的,我們一般稱之為重量級鎖焰薄。因為 OS 實現(xiàn)線程間的切換需要從用戶態(tài)轉(zhuǎn)換到核心態(tài)拿诸,這個轉(zhuǎn)換過程成本較高,耗時相對較長塞茅,因此 synchronized 效率會比較低亩码。
重量級鎖的鎖標志位為’10’,指針指向的是 monitor 對象的起始地址
輕量級鎖
輕量級鎖是相對基于OS的互斥量實現(xiàn)的重量級鎖而言的野瘦,它的本意是在沒有多線程競爭的前提下描沟,減少傳統(tǒng)的重量級鎖使用OS的互斥量而帶來的性能消耗。
輕量級鎖提升性能的經(jīng)驗依據(jù)是:對于絕大部分鎖鞭光,在整個同步周期內(nèi)都是不存在競爭的吏廉。如果沒有競爭,輕量級鎖就可以使用 CAS 操作避免互斥量的開銷惰许,從而提升效率席覆。
輕量級鎖的加鎖過程:
1、線程在進入到同步代碼塊的時候汹买,JVM 會先在當前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間佩伤,用于存儲鎖對象當前 Mark Word 的拷貝(官方稱為 Displaced Mark Word),owner 指針指向?qū)ο蟮?Mark Word晦毙。此時堆棧與對象頭的狀態(tài)如圖所示:
2生巡、JVM 使用 CAS 操作嘗試將對象頭中的 Mark Word 更新為指向 Lock Record 的指針。如果更新成功见妒,則執(zhí)行步驟3孤荣;更新失敗,則執(zhí)行步驟4
3、如果更新成功垃环,那么這個線程就擁有了該對象的鎖邀层,對象的 Mark Word 的鎖狀態(tài)為輕量級鎖(標志位轉(zhuǎn)變?yōu)椤?0’)。此時線程堆棧與對象頭的狀態(tài)如圖所示:
4遂庄、如果更新失敗寥院,JVM 首先檢查對象的 Mark Word 是否指向當前線程的棧幀
如果是,就說明當前線程已經(jīng)擁有了該對象的鎖涛目,那就可以直接進入同步代碼塊繼續(xù)執(zhí)行
如果不是秸谢,就說明這個鎖對象已經(jīng)被其他的線程搶占了,當前線程會嘗試自旋一定次數(shù)來獲取鎖霹肝。如果自旋一定次數(shù) CAS 操作仍沒有成功估蹄,那么輕量級鎖就要升級為重量級鎖(鎖的標志位轉(zhuǎn)變?yōu)椤?0’),Mark Word 中存儲的就是指向重量級鎖的指針沫换,后面等待鎖的線程也就進入阻塞狀態(tài)
輕量級鎖的解鎖過程:
1臭蚁、通過 CAS 操作用線程中復制的 Displaced Mark Word 中的數(shù)據(jù)替換對象當前的 Mark Word
2、如果替換成功讯赏,整個同步過程就完成了
3垮兑、如果替換失敗,說明有其他線程嘗試過獲取該鎖漱挎,那就在釋放鎖的同時系枪,喚醒被掛起的線程
偏向鎖
輕量級鎖是在無多線程競爭的情況下,使用 CAS 操作去消除互斥量磕谅;偏向鎖是在無多線程競爭的情況下私爷,將這個同步都消除掉。
偏向鎖提升性能的經(jīng)驗依據(jù)是:對于絕大部分鎖膊夹,在整個同步周期內(nèi)不僅不存在競爭衬浑,而且總由同一線程多次獲得。偏向鎖會偏向第一個獲得它的線程割疾,如果接下來的執(zhí)行過程中嚎卫,該鎖沒有被其他線程獲取,則持有偏向鎖的線程不需要再進行同步宏榕。這使得線程獲取鎖的代價更低拓诸。
偏向鎖的獲取過程:
1、線程執(zhí)行同步塊麻昼,鎖對象第一次被獲取的時候奠支,JVM 會將鎖對象的 Mark Word 中的鎖狀態(tài)設置為偏向鎖(鎖標志位為’01’,是否偏向的標志位為’1’)倍谜,同時通過 CAS 操作在 Mark Word 中記錄獲取到這個鎖的線程的 ThreadID
2、如果 CAS 操作成功答毫。持有偏向鎖的線程每次進入和退出同步塊時洗搂,只需測試一下 Mark Word 里是否存儲著當前線程的 ThreadID耘拇。如果是惫叛,則表示線程已經(jīng)獲得了鎖嘉涌,而不需要額外花費 CAS 操作加鎖和解鎖
3洛心、如果不是,則通過CAS操作競爭鎖厅目,競爭成功损敷,則將 Mark Word 的 ThreadID 替換為當前線程的 ThreadID
偏向鎖的釋放過程:
1拗馒、當一個線程已經(jīng)持有偏向鎖洋丐,而另外一個線程嘗試競爭偏向鎖時友绝,CAS 替換 ThreadID 操作失敗迁客,則開始撤銷偏向鎖掷漱。偏向鎖的撤銷卜范,需要等待原持有偏向鎖的線程到達全局安全點(在這個時間點上沒有字節(jié)碼正在執(zhí)行)缰冤,暫停該線程棉浸,并檢查其狀態(tài)
2刺彩、如果原持有偏向鎖的線程不處于活動狀態(tài)或已退出同步代碼塊创倔,則該線程釋放鎖畦攘。將對象頭設置為無鎖狀態(tài)(鎖標志位為’01’知押,是否偏向標志位為’0’)
3台盯、如果原持有偏向鎖的線程未退出同步代碼塊良价,則升級為輕量級鎖(鎖標志位為’00’)
下面是這幾種鎖的比較:
偏向鎖、輕量級鎖袖外、重量級鎖之間的狀態(tài)轉(zhuǎn)換如圖所示:
其他優(yōu)化
1、適應性自旋
自旋鎖:互斥同步時魂务,掛起和恢復線程都需要切換到內(nèi)核態(tài)完成曼验,這對性能并發(fā)帶來了不少的壓力泌射。同時在許多應用上,共享數(shù)據(jù)的鎖定狀態(tài)只會持續(xù)很短的一段時間鬓照,為了這段較短的時間而去掛起和恢復線程并不值得熔酷。那么如果有多個線程同時并行執(zhí)行,可以讓后面請求鎖的線程通過自旋(CPU忙循環(huán)執(zhí)行空指令)的方式稍等一會兒豺裆,看看持有鎖的線程是否會很快的釋放鎖拒秘,這樣就不需要放棄 CPU 的執(zhí)行時間了。
適應性自旋:在輕量級鎖獲取過程中臭猜,線程執(zhí)行 CAS 操作失敗時躺酒,需要通過自旋來獲取重量級鎖。如果鎖被占用的時間比較短蔑歌,那么自旋等待的效果就會比較好羹应,而如果鎖占用的時間很長,自旋的線程則會白白浪費 CPU 資源次屠。解決這個問題的最簡答的辦法就是:指定自旋的次數(shù)园匹,如果在限定次數(shù)內(nèi)還沒獲取到鎖(例如10次),就按傳統(tǒng)的方式掛起線程進入阻塞狀態(tài)劫灶。JDK1.6 之后引入了自適應性自旋的方式裸违,如果在同一鎖對象上,一線程自旋等待剛剛成功獲得鎖本昏,并且持有鎖的線程正在運行中供汛,那么 JVM 會認為這次自旋也有可能再次成功獲得鎖,進而允許自旋等待相對更長的時間(例如100次)涌穆。另一方面紊馏,如果某個鎖自旋很少成功獲得,那么以后要獲得這個鎖時將省略自旋過程蒲犬,以避免浪費 CPU。
2岸啡、鎖消除
鎖消除就是編譯器運行時原叮,對一些被檢測到不可能存在共享數(shù)據(jù)競爭的鎖進行消除。如果判斷一段代碼中巡蘸,堆上的數(shù)據(jù)不會逃逸出去從而被其他線程訪問到奋隶,則可以把他們當做棧上的數(shù)據(jù)對待,認為它們是線程私有的悦荒,不必要加鎖唯欣。
<pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px;">public String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
return sb.toString();
}
</pre>
在 StringBuffer.append() 方法中有一個同步代碼塊,鎖就是sb對象搬味,但 sb 的所有引用不會逃逸到 concatString() 方法外部境氢,其他線程無法訪問它蟀拷。因此這里有鎖,但是在即時編譯之后萍聊,會被安全的消除掉问芬,忽略掉同步而直接執(zhí)行了。
3寿桨、鎖粗化
鎖粗化就是 JVM 檢測到一串零碎的操作都對同一個對象加鎖此衅,則會把加鎖同步的范圍粗化到整個操作序列的外部。以上述 concatString() 方法為例亭螟,內(nèi)部的 StringBuffer.append() 每次都會加鎖挡鞍,將會鎖粗化,在第一次 append() 前至 最后一個 append() 后只需要加一次鎖就可以了预烙。
關(guān)注墨微、轉(zhuǎn)發(fā)、評論 每天分享java 知識默伍,私信回復“源碼”贈送Spring源碼分析欢嘿、Dubbo、Redis也糊、Netty炼蹦、zookeeper、Spring cloud狸剃、分布式資料