「BATJ面試系列」并發(fā)編程之synchronized實現(xiàn)原理

在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挥萌。

過程如下圖所示:

「BATJ面試系列」并發(fā)編程之synchronized實現(xiàn)原理

鎖優(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 如下圖所示:

「BATJ面試系列」并發(fā)編程之synchronized實現(xiàn)原理

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)如圖所示:

「BATJ面試系列」并發(fā)編程之synchronized實現(xiàn)原理

2生巡、JVM 使用 CAS 操作嘗試將對象頭中的 Mark Word 更新為指向 Lock Record 的指針。如果更新成功见妒,則執(zhí)行步驟3孤荣;更新失敗,則執(zhí)行步驟4

3、如果更新成功垃环,那么這個線程就擁有了該對象的鎖邀层,對象的 Mark Word 的鎖狀態(tài)為輕量級鎖(標志位轉(zhuǎn)變?yōu)椤?0’)。此時線程堆棧與對象頭的狀態(tài)如圖所示:

「BATJ面試系列」并發(fā)編程之synchronized實現(xiàn)原理

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’)

下面是這幾種鎖的比較:

「BATJ面試系列」并發(fā)編程之synchronized實現(xiàn)原理

偏向鎖、輕量級鎖袖外、重量級鎖之間的狀態(tài)轉(zhuǎn)換如圖所示:

「BATJ面試系列」并發(fā)編程之synchronized實現(xià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狸剃、分布式資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末掐隐,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子钞馁,更是在濱河造成了極大的恐慌虑省,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件僧凰,死亡現(xiàn)場離奇詭異探颈,居然都是意外死亡,警方通過查閱死者的電腦和手機训措,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進店門伪节,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人绩鸣,你說我怎么就攤上這事怀大。” “怎么了呀闻?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵化借,是天一觀的道長。 經(jīng)常有香客問我捡多,道長蓖康,這世上最難降的妖魔是什么铐炫? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮钓瞭,結(jié)果婚禮上驳遵,老公的妹妹穿的比我還像新娘。我一直安慰自己山涡,他們只是感情好堤结,可當我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著鸭丛,像睡著了一般竞穷。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鳞溉,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天瘾带,我揣著相機與錄音,去河邊找鬼熟菲。 笑死看政,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的抄罕。 我是一名探鬼主播允蚣,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼呆贿!你這毒婦竟也來了嚷兔?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤做入,失蹤者是張志新(化名)和其女友劉穎冒晰,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體竟块,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡壶运,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了浪秘。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片前弯。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖秫逝,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情询枚,我是刑警寧澤违帆,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站金蜀,受9級特大地震影響刷后,放射性物質(zhì)發(fā)生泄漏的畴。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一尝胆、第九天 我趴在偏房一處隱蔽的房頂上張望丧裁。 院中可真熱鬧,春花似錦含衔、人聲如沸煎娇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽缓呛。三九已至,卻和暖如春杭隙,著一層夾襖步出監(jiān)牢的瞬間哟绊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工痰憎, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留票髓,地道東北人。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓铣耘,卻偏偏與公主長得像洽沟,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子涡拘,可洞房花燭夜當晚...
    茶點故事閱讀 45,512評論 2 359

推薦閱讀更多精彩內(nèi)容