Java鎖升級

前言

本篇文章介紹Java Synchronized鎖優(yōu)化番甩。

  1. 鎖是存在哪里的瓮顽,怎么標(biāo)識是什么鎖
  2. Monitor機制在Java中怎么表現(xiàn)的
  3. 鎖優(yōu)化
  4. 鎖升級

1. 鎖存在哪里

  • 對象在內(nèi)存中的布局分為三塊區(qū)域:對象頭铁追、實例數(shù)據(jù)和對齊填充驱证。
    Hotspot虛擬機的對象頭主要包括兩部分?jǐn)?shù)據(jù):Mark Word(標(biāo)記字段)蜻势、Klass Pointer(類型指針)等限,數(shù)組會多1字寬(32位: 4字節(jié))來存儲數(shù)組長度有巧。
  • synchronized用的鎖是存在Java對象頭里的释漆。
    其中Klass Point是對象指向它的類元數(shù)據(jù)的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例篮迎;Mark Word用于存儲對象自身的運行時數(shù)據(jù)男图,它是實現(xiàn)輕量級鎖和偏向鎖的關(guān)鍵示姿。

JVM中對象頭的方式有以下兩種(以32位JVM為例):

// 普通對象
|--------------------------------------------------------------|
|                     Object Header (64 bits)                  |
|------------------------------------|-------------------------|
|        Mark Word (32 bits)         |    Klass Word (32 bits) |
|------------------------------------|-------------------------|

// 數(shù)組對象
|---------------------------------------------------------------------------------|
|                                 Object Header (96 bits)                         |
|--------------------------------|-----------------------|------------------------|
|        Mark Word(32bits)       |    Klass Word(32bits) |  array length(32bits)  |
|--------------------------------|-----------------------|------------------------|

  • Mark Word
    這部分主要用來存儲對象自身的運行時數(shù)據(jù),如hashcode逊笆、gc分代年齡栈戳、鎖狀態(tài)標(biāo)志、線程持有的鎖难裆、偏向線程 ID子檀、偏向時間戳等等。
    mark word的位長度為JVM的一個Word大小乃戈,也就是說32位JVM的Mark word為32位褂痰,64位JVM為64位。
    Mark Word被設(shè)計成一個非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存存儲盡量多的數(shù)據(jù)症虑,它會根據(jù)對象的狀態(tài)復(fù)用自己的存儲空間缩歪,為了讓一個字大小存儲更多的信息,JVM將字的最低兩個位設(shè)置為標(biāo)記位谍憔,不同標(biāo)記位下的Mark Word示意如下:
|-------------------------------------------------------|--------------------|
|                  Mark Word (32 bits)                  |       State        |
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:0 |lock:01 |     Normal無鎖      |
|-------------------------------------------------------|--------------------|
|  thread:23 | epoch:2 | age:4 | biased_lock:1| lock:01 |     Biased偏向鎖    |
|-------------------------------------------------------|--------------------|
|               ptr_to_lock_record:30         | lock:00 | Lightweight Locked輕量級鎖 |
|-------------------------------------------------------|--------------------|
|               ptr_to_heavyweight_monitor:30 | lock:10 | Heavyweight Locked重量級鎖 |
|-------------------------------------------------------|--------------------|
|                                             | lock:11 |    Marked for GC   GC標(biāo)記|
|-------------------------------------------------------|--------------------|

image
  • lock:2位的鎖狀態(tài)標(biāo)記位匪蝙,由于希望用盡可能少的二進制位表示盡可能多的信息,所以設(shè)置了lock標(biāo)記习贫。該標(biāo)記的值不同逛球,整個mark word表示的含義不同。
  • biased_lock:對象是否啟用偏向鎖標(biāo)記苫昌,只占1個二進制位颤绕。為1時表示對象啟用偏向鎖,為0時表示對象沒有偏向鎖蜡歹。
  • age:4位的Java對象年齡屋厘。在GC中涕烧,如果對象在Survivor區(qū)復(fù)制一次月而,年齡增加1。當(dāng)對象達(dá)到設(shè)定的閾值時议纯,將會晉升到老年代父款。默認(rèn)情況下,并行GC的年齡閾值為15瞻凤,并發(fā)GC的年齡閾值為6憨攒。由于age只有4位,所以最大值為15阀参,這就是-XX:MaxTenuringThreshold選項最大值為15的原因肝集。
  • identity_hashcode:25位的對象標(biāo)識Hash碼,采用延遲加載技術(shù)蛛壳。調(diào)用方法System.identityHashCode()計算杏瞻,并會將結(jié)果寫到該對象頭中所刀。當(dāng)對象被鎖定時,該值會移動到管程Monitor中捞挥。
  • thread:持有偏向鎖的線程ID浮创。
  • epoch:偏向時間戳。
  • ptr_to_lock_record:指向棧中鎖記錄的指針砌函。
  • ptr_to_heavyweight_monitor:指向管程Monitor的指針斩披。

2. Monitor機制

Monitor其實是一種同步工具、同步機制讹俊,在Java中垦沉,Object 類本身就是監(jiān)視者對象,Java 對于 Monitor Object 模式做了內(nèi)建的支持仍劈,即每一個Java對象是天生的Monitor乡话,每一個Java對象都有成為Monitor的潛質(zhì)。
并且同時只能有一個線程可以獲得該對象monitor的所有權(quán)耳奕。在線程進入時通過monitorenter嘗試取得對象monitor所有權(quán)绑青,退出時通過monitorexit釋放對象monitor所有權(quán)。

  • monitorenter過程如下:
    如果 monitor 的進入數(shù)為0屋群,則該線程進入 monitor闸婴,然后將進入數(shù)設(shè)置為1,該線程即為 monitor 的所有者芍躏;
    如果線程已經(jīng)占有monitor邪乍,只是重新進入,則monitor的進入數(shù)+1对竣;
    如果其他線程已經(jīng)占用 monitor庇楞,則該線程處于阻塞狀態(tài),直至 monitor 的進入數(shù)為0否纬,再重新嘗試獲得 monitor 的所有權(quán)
  • monitorexit:
    執(zhí)行 monitorexit 的線程必須是 objectref 所對應(yīng)的 monitor 的所有者吕晌。執(zhí)行指令時,monitor 的進入數(shù)減1临燃,如果減1后進入數(shù)為0睛驳,則線程退出 monitor,不再是這個 monitor 的所有者膜廊,其他被這個 monitor 阻塞的線程可以嘗試獲取這個 monitor 的所有權(quán)乏沸。

Monitor 是線程私有的數(shù)據(jù)結(jié)構(gòu),每一個線程都有一個可用monitor record列表爪瓜,同時還有一個全局的可用列表蹬跃。
每一個被鎖住的對象都會和一個monitor關(guān)聯(lián)(對象頭的MarkWord中的LockWord指向monitor的起始地址),同時monitor中有一個Owner字段存放擁有該鎖的線程的唯一標(biāo)識铆铆,表示該鎖被這個線程占用蝶缀。

image
  • Owner:初始時為NULL表示當(dāng)前沒有任何線程擁有該monitor record辆苔,當(dāng)線程成功擁有該鎖后保存線程唯一標(biāo)識,當(dāng)鎖被釋放時又設(shè)置為NULL扼劈;
  • EntryQ:關(guān)聯(lián)一個系統(tǒng)互斥鎖(semaphore)驻啤,阻塞所有試圖鎖住monitor record失敗的線程。
  • RcThis:表示blocked或waiting在該monitor record上的所有線程的個數(shù)荐吵。
  • Nest:用來實現(xiàn)重入鎖的計數(shù)骑冗。
  • HashCode:保存從對象頭拷貝過來的HashCode值(可能還包含GC age)。
  • Candidate:用來避免不必要的阻塞或等待線程喚醒先煎,因為每一次只有一個線程能夠成功擁有鎖贼涩,如果每次前一個釋放鎖的線程喚醒所有正在阻塞或等待的線程,會引起不必要的上下文切換(從阻塞到就緒然后因為競爭鎖失敗又被阻塞)從而導(dǎo)致性能嚴(yán)重下降薯蝎。Candidate只有兩種可能的值遥倦,0表示沒有需要喚醒的線程,1表示要喚醒一個繼任線程來競爭鎖占锯。

3. 鎖優(yōu)化

jdk1.6對鎖的實現(xiàn)引入了大量的優(yōu)化袒哥,如自旋鎖、適應(yīng)性自旋鎖消略、鎖消除堡称、鎖粗化、偏向鎖艺演、輕量級鎖等技術(shù)來減少鎖操作的開銷却紧。
鎖主要存在四中狀態(tài),依次是:無鎖狀態(tài)胎撤、偏向鎖狀態(tài)晓殊、輕量級鎖狀態(tài)、重量級鎖狀態(tài)伤提,他們會隨著競爭的激烈而逐漸升級巫俺。
其中鎖可以升級不可降級,這種策略是為了提高獲得鎖和釋放鎖的效率飘弧。

1. 重量級鎖

monitor 監(jiān)視器鎖本質(zhì)上是依賴操作系統(tǒng)的 Mutex Lock 互斥量 來實現(xiàn)的识藤,我們一般稱之為重量級鎖砚著。因為 OS 實現(xiàn)線程間的切換需要從用戶態(tài)轉(zhuǎn)換到核心態(tài)次伶,這個轉(zhuǎn)換過程成本較高,耗時相對較長稽穆,因此 synchronized 效率會比較低冠王。

2. 輕量級鎖

輕量級鎖,其性能提升的依據(jù)是對于絕大部分的鎖舌镶,在整個生命周期內(nèi)都是不會存在競爭的柱彻,如果沒有競爭豪娜,輕量級鎖就可以使用 CAS 操作避免互斥量的開銷,從而提升效率哟楷。
如果打破這個依據(jù)則除了互斥的開銷外瘤载,還有額外的CAS操作,因此在有多線程競爭的情況下卖擅,輕量級鎖比重量級鎖更慢鸣奔。

  • 輕量級鎖的加鎖過程:

    1. 線程在進入到同步代碼塊的時候,JVM 會先在當(dāng)前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間惩阶,用于存儲鎖對象當(dāng)前 Mark Word 的拷貝(官方稱為 Displaced Mark Word)挎狸,owner 指針指向?qū)ο蟮?Mark Word。此時堆棧與對象頭的狀態(tài)如圖所示:

      image
    2. JVM 使用 CAS 操作嘗試將對象頭中的 Mark Word 更新為指向 Lock Record 的指針断楷。如果更新成功锨匆,則執(zhí)行步驟3;更新失敗冬筒,則執(zhí)行步驟4

    3. 如果更新成功恐锣,那么這個線程就擁有了該對象的鎖,對象的 Mark Word 的鎖狀態(tài)為輕量級鎖(標(biāo)志位轉(zhuǎn)變?yōu)?00')舞痰。此時線程堆棧與對象頭的狀態(tài)如圖所示:

      image
    4. 如果更新失敗侥蒙,JVM 首先檢查對象的 Mark Word 是否指向當(dāng)前線程的棧幀
      如果是,就說明當(dāng)前線程已經(jīng)擁有了該對象的鎖匀奏,那就可以直接進入同步代碼塊繼續(xù)執(zhí)行
      如果不是鞭衩,就說明這個鎖對象已經(jīng)被其他的線程搶占了,當(dāng)前線程會嘗試自旋一定次數(shù)來獲取鎖娃善。如果自旋一定次數(shù) CAS 操作仍沒有成功论衍,那么輕量級鎖就要升級為重量級鎖(鎖的標(biāo)志位轉(zhuǎn)變?yōu)?10'),Mark Word 中存儲的就是指向重量級鎖的指針聚磺,后面等待鎖的線程也就進入阻塞狀態(tài)

  • 輕量級鎖的解鎖過程:

    1. 通過 CAS 操作用線程中復(fù)制的 Displaced Mark Word 中的數(shù)據(jù)替換對象當(dāng)前的 Mark Word
    2. 如果替換成功坯台,整個同步過程就完成了
    3. 如果替換失敗,說明有其他線程嘗試過獲取該鎖瘫寝,那就在釋放鎖的同時蜒蕾,喚醒被掛起的線程

3. 偏向鎖

依據(jù):對于絕大部分鎖,在整個同步周期內(nèi)不僅不存在競爭焕阿,而且總由同一線程多次獲得咪啡。
在一些情況下總是同一線程多次獲得鎖,此時第二次再重新做CAS修改對象頭中的Mark Word這樣的操作暮屡,有些多余撤摸。所以就有了偏向鎖,只需要檢查是否為偏向鎖、鎖標(biāo)識為以及ThreadID即可准夷,只要是同一線程就不再修改對象頭钥飞。其目的為了在無多線程競爭的情況下盡量減少不必要的輕量級鎖執(zhí)行路徑。

  • 偏向鎖枷鎖過程:

    1. 檢測Mark Word是否為可偏向狀態(tài)衫嵌,即是否為偏向鎖1读宙,鎖標(biāo)識位為01;
    2. 若為可偏向狀態(tài)楔绞,則測試線程ID是否為當(dāng)前線程ID论悴,如果是,則執(zhí)行步驟(5)墓律,否則執(zhí)行步驟(3)膀估;
    3. 如果線程ID不為當(dāng)前線程ID,則通過CAS操作競爭鎖耻讽,競爭成功察纯,則將Mark Word的線程ID替換為當(dāng)前線程ID,否則執(zhí)行線程(4)针肥;
    4. 通過CAS競爭鎖失敗饼记,證明當(dāng)前存在多線程競爭情況,當(dāng)?shù)竭_(dá)全局安全點慰枕,獲得偏向鎖的線程被掛起具则,偏向鎖升級為輕量級鎖,然后被阻塞在安全點的線程繼續(xù)往下執(zhí)行同步代碼塊具帮;
    5. 執(zhí)行同步代碼塊
  • 偏向鎖釋放過程:

    1. 當(dāng)一個線程已經(jīng)持有偏向鎖博肋,而另外一個線程嘗試競爭偏向鎖時,CAS 替換 ThreadID 操作失敗蜂厅,則開始撤銷偏向鎖匪凡。偏向鎖的撤銷,需要等待原持有偏向鎖的線程到達(dá)全局安全點(在這個時間點上沒有字節(jié)碼正在執(zhí)行)掘猿,暫停該線程病游,并檢查其狀態(tài)
    2. 如果原持有偏向鎖的線程不處于活動狀態(tài)或已退出同步代碼塊,則該線程釋放鎖稠通。將對象頭設(shè)置為無鎖狀態(tài)(鎖標(biāo)志位為'01'衬衬,是否偏向標(biāo)志位為'0')
    3. 如果原持有偏向鎖的線程未退出同步代碼塊,則升級為輕量級鎖(鎖標(biāo)志位為'00')

4. 總結(jié)

鎖主要存在四中狀態(tài)改橘,依次是:無鎖狀態(tài)滋尉、偏向鎖狀態(tài)、輕量級鎖狀態(tài)唧龄、重量級鎖狀態(tài)兼砖,其升級如下圖所示:

image
image

其他優(yōu)化

  • 自旋鎖:
    互斥同步時奸远,掛起和恢復(fù)線程都需要切換到內(nèi)核態(tài)完成既棺,這對性能并發(fā)帶來了不少的壓力讽挟。同時在許多應(yīng)用上,共享數(shù)據(jù)的鎖定狀態(tài)只會持續(xù)很短的一段時間丸冕,為了這段較短的時間而去掛起和恢復(fù)線程并不值得耽梅。那么如果有多個線程同時并行執(zhí)行,可以讓后面請求鎖的線程通過自旋(CPU忙循環(huán)執(zhí)行空指令)的方式稍等一會兒胖烛,看看持有鎖的線程是否會很快的釋放鎖眼姐,這樣就不需要放棄 CPU 的執(zhí)行時間了。

  • 適應(yīng)性自旋:
    自旋時如果鎖被占用的時間比較短佩番,那么自旋等待的效果就會比較好众旗,而如果鎖占用的時間很長,自旋的線程則會白白浪費 CPU 資源趟畏。解決這個問題的最簡答的辦法就是:指定自旋的次數(shù)贡歧,如果在限定次數(shù)內(nèi)還沒獲取到鎖(例如10次),就按傳統(tǒng)的方式掛起線程進入阻塞狀態(tài)赋秀。
    JDK1.6 之后引入了自適應(yīng)性自旋的方式利朵,如果在同一鎖對象上,一線程自旋等待剛剛成功獲得鎖猎莲,并且持有鎖的線程正在運行中绍弟,那么 JVM 會認(rèn)為這次自旋也有可能再次成功獲得鎖,進而允許自旋等待相對更長的時間(例如100次)著洼。另一方面樟遣,如果某個鎖自旋很少成功獲得,那么以后要獲得這個鎖時將省略自旋過程身笤,以避免浪費 CPU年碘。

  • 鎖消除
    虛擬機即時編譯器(JIT)運行時,依據(jù)逃逸分析的數(shù)據(jù)檢測到不可能存在競爭的鎖展鸡,就自動將該鎖消除)屿衅。鎖消除的依據(jù)是逃逸分析的數(shù)據(jù)支持。
    如果判斷一段代碼中莹弊,堆上的數(shù)據(jù)不會逃逸出去從而被其他線程訪問到涤久,則可以把他們當(dāng)做棧上的數(shù)據(jù)對待,認(rèn)為它們是線程私有的忍弛,不必要加鎖响迂。
    如下所示,在 StringBuffer.append() 方法中有一個同步代碼塊细疚,鎖就是sb對象蔗彤,但 sb 的所有引用不會逃逸到 concatString() 方法外部,其他線程無法訪問它。因此這里有鎖然遏,但是在即時編譯之后贫途,會被安全的消除掉,忽略掉同步而直接執(zhí)行了待侵。

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();
}

  • 鎖粗化
    鎖粗化就是 JVM 檢測到一串零碎的操作都對同一個對象加鎖丢早,則會把加鎖同步的范圍粗化到整個操作序列的外部。
    以上述 concatString() 方法為例秧倾,內(nèi)部的 StringBuffer.append() 每次都會加鎖怨酝,將會鎖粗化,在第一次 append() 前至 最后一個 append() 后只需要加一次鎖就可以了那先。

結(jié)語

本篇文章介紹了JVM對Synchronized進行的鎖優(yōu)化农猬。自旋到自適應(yīng)自旋、鎖消除和鎖粗化售淡,從無鎖到偏向鎖斤葱、輕量級鎖,都在避免線程進入內(nèi)核態(tài)進行的切換勋又。針對各種情況做的優(yōu)化苦掘,

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市楔壤,隨后出現(xiàn)的幾起案子鹤啡,更是在濱河造成了極大的恐慌,老刑警劉巖蹲嚣,帶你破解...
    沈念sama閱讀 212,718評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件递瑰,死亡現(xiàn)場離奇詭異,居然都是意外死亡隙畜,警方通過查閱死者的電腦和手機抖部,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來议惰,“玉大人慎颗,你說我怎么就攤上這事⊙匝” “怎么了俯萎?”我有些...
    開封第一講書人閱讀 158,207評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長运杭。 經(jīng)常有香客問我夫啊,道長,這世上最難降的妖魔是什么辆憔? 我笑而不...
    開封第一講書人閱讀 56,755評論 1 284
  • 正文 為了忘掉前任撇眯,我火速辦了婚禮报嵌,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘熊榛。我一直安慰自己锚国,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,862評論 6 386
  • 文/花漫 我一把揭開白布来候。 她就那樣靜靜地躺著跷叉,像睡著了一般逸雹。 火紅的嫁衣襯著肌膚如雪营搅。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,050評論 1 291
  • 那天梆砸,我揣著相機與錄音转质,去河邊找鬼。 笑死帖世,一個胖子當(dāng)著我的面吹牛休蟹,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播日矫,決...
    沈念sama閱讀 39,136評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼赂弓,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了哪轿?” 一聲冷哼從身側(cè)響起盈魁,我...
    開封第一講書人閱讀 37,882評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎窃诉,沒想到半個月后杨耙,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,330評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡飘痛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,651評論 2 327
  • 正文 我和宋清朗相戀三年珊膜,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片宣脉。...
    茶點故事閱讀 38,789評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡车柠,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出塑猖,到底是詐尸還是另有隱情竹祷,我是刑警寧澤,帶...
    沈念sama閱讀 34,477評論 4 333
  • 正文 年R本政府宣布萌庆,位于F島的核電站溶褪,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏践险。R本人自食惡果不足惜猿妈,卻給世界環(huán)境...
    茶點故事閱讀 40,135評論 3 317
  • 文/蒙蒙 一吹菱、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧彭则,春花似錦鳍刷、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至芬萍,卻和暖如春尤揣,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背柬祠。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評論 1 267
  • 我被黑心中介騙來泰國打工北戏, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人漫蛔。 一個月前我還...
    沈念sama閱讀 46,598評論 2 362
  • 正文 我出身青樓嗜愈,卻偏偏與公主長得像,于是被迫代替她去往敵國和親莽龟。 傳聞我的和親對象是個殘疾皇子蠕嫁,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,697評論 2 351

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