Java併發-synchronized, 偏向鎖, 輕量級鎖詳解

synchronized概述

synchronized就是所謂的重量級鎖, 但是自從jdk1.6引入了偏向鎖, 輕量級鎖之後, synchronized就沒有那麼重了。

synchronized用法

  • 對於普通同步方法,鎖是當前實例對象
  • 對於靜態同步方法,鎖是當前類的Class對象
  • 對於同步方法塊,鎖是Synchonized括號裏配置的對象

synchronized實現原理

  • 任何對象都有一個monitor與之關聯,當且一個monitor被持有後,它將處於鎖定狀態
  • 使用monitorenter和monitorexit指令實現
    • monitorenter指令是在編譯後插入到同步代碼塊的開始位置
    • 線程執行到monitorenter指令時,將會嘗試獲取對象所對應的monitor的所有權,即嘗試獲得對象的鎖
    • monitorexit是插入到方法結束處和異常處
    • JVM要保證每個monitorenter必須有對應的monitorexit與之配對
  • synchronized用的鎖是存在Java對象頭裏的

Java對象頭

Java對象頭的長度

長 度 內 容 說 明
32/64bit Mark Word 存儲對象的hashCode或鎖信息
32/64bit Class Metadata Address 存儲到對象類型數據的指針
32/64bit Array length 數組的長度(如果當前對象是數組)

Java對象頭的存儲結構

Java對象頭裏的Mark Word裏默認存儲對象的HashCode、分代年齡和鎖標記位

32位JVM的Mark Word的默認存儲結構 :

鎖狀態 25bit 4bit 1bit是否偏向鎖 2bit鎖標誌位
無鎖狀態 對象的hashCode 對象的分代年齡 0 01

64位JVM的Mark Word的默認存儲結構 :

鎖狀態 25bit 31bit 1bit 4bit 1bit 2bit
cms_free 分代年齡 偏向鎖 鎖標誌位
無鎖 unused hasCode 0 01
偏向鎖 ThreadID(51bit)Epoch(2bit) 1 01

在運行期間,Mark Word裏存儲的數據會隨着鎖標誌位的變化而變化。Mark Word可能變化爲存儲以下4種數據:

鎖狀態
25bit
4bit
1bit
2bit
23bit 2bit 是否偏向鎖 鎖標誌位
輕量級鎖 指向棧中鎖記錄的指針 00
重量級鎖 指向互斥量(重量級鎖)的指針 10
GC標記
11
偏向鎖 線程ID Epoch 對象分代年齡 1 01

鎖的升級與對比

鎖狀態

在JDK1.6之後, 鎖存在四種狀態, 級別從低到高依次是 :

  • 無鎖狀態
  • 偏向鎖狀態
  • 輕量級鎖狀態
  • 重量級鎖狀態

這幾個狀態會隨着競爭情況逐漸升級。鎖可以升級但不能降級,意味着偏向鎖升級成輕量級鎖後不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是爲了提高獲得鎖和釋放鎖的效率。

偏向鎖

偏向鎖引入原因:

由於大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,爲了讓線程獲得鎖的代價更低而引入了偏向鎖

偏向鎖獲取流程

偏向鎖獲取流程

流程說明 :

  1. 線程進入同步代碼塊時, 先判斷對象頭的Mark Word是否無鎖狀態,是否可偏向(鎖標誌位01, 偏向鎖狀態爲0), 是的話CAS設置偏向鎖狀態爲1, 表示啓用偏向鎖, 並將偏向鎖指向當前線程然後執行步驟6, 否則的話繼續進行下面的判斷
  2. 判斷對象頭的Mark Word中是否存儲着指向當前線程的偏向鎖, 如果是表示獲取偏向鎖成功, 則執行步驟6, 否則執行步驟3
  3. 判斷Mark Word中偏向鎖標識是否設置爲1(表示當前是偏向鎖), 如果是的話指向步驟4 ,否則執行步驟5
  4. 嘗試使用CAS將對象頭的偏向鎖指向當前線程, 成功表示獲取偏向鎖成功, 則執行步驟6, 失敗則表示存在競爭, 偏向鎖要升級爲輕量級鎖, 偏向鎖撤銷和升級的流程下面再進行說明
  5. 表示已經不是偏向鎖了, 使用CAS競爭鎖
  6. 執行同步代碼塊

偏向鎖撤銷

偏向鎖獲取和撤銷流程

線程1獲取偏向鎖的流程和上面偏向鎖獲取流程一致, 這裏就省略了, 從線程2開始對上述流程做一個說明 :

  1. 線程2訪問同步代碼塊, 發現對象頭Mark Word中偏向鎖標誌爲1, 鎖標誌位爲01, 表示可偏向, 因爲線程1已經獲取了偏向鎖, 這個時候對象頭的狀態已經由線程1更新爲偏向鎖狀態了

  2. 檢查對象頭中偏向鎖是否指向了線程2, 發現並不是,這時還是指向線程1

  3. 嘗試使用CAS將對象頭的偏向鎖指向當前線程, CAS替換Mark Word成功表示獲取偏向鎖成功, 這裏由於對象頭中Mark Word已經指向了線程1, 所以替換失敗, 需要撤銷偏向鎖

    這裏關於CAS替換Mark Word這一步, 個人的理解就是, 一個偏向鎖只能由一個線程獲得, 如果第二個線程來試圖獲取偏向鎖時, 偏向模式就宣告結束。根據所對象目前是否處於被鎖定狀態, 執行撤銷偏向鎖恢復到無鎖狀態,或者將偏向鎖升級爲輕量級鎖狀態

  4. 撤銷偏向鎖, 需要等待全局安全點(safepoint)

  5. 首先暫停擁有偏向鎖的線程, 檢查持有偏向鎖的線程是否存活 , 如果線程存活, 則鎖升級爲輕量級鎖, 否則進行偏向鎖撤銷

  6. 偏向鎖撤銷之後, 恢復線程1, 線程2再去以偏向模式獲取偏向鎖

偏向鎖關閉

  • 偏向鎖是默認開啓的,而且開始時間一般是比應用程序啓動慢幾秒,如果不想有這個延遲,那麼可以使用-XX:BiasedLockingStartUpDelay=0;

  • 如果不想要偏向鎖,那麼可以通過-XX:-UseBiasedLocking = false來設置;

輕量級鎖

輕量級鎖加鎖

線程在執行同步塊之前,JVM會先在當前線程的棧楨中創建用於存儲鎖記錄的空間,並將對象頭中的Mark Word複製到鎖記錄中,官方稱爲Displaced Mark Word。然後線程嘗試使用CAS將對象頭中的Mark Word替換爲指向鎖記錄的指針。如果成功,當前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。

輕量級鎖解鎖

輕量級解鎖時,會使用原子的CAS操作將Displaced Mark Word替換回到對象頭,如果成功,則表示沒有競爭發生。如果失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。下圖就是競爭導致鎖膨脹的流程圖:

輕量級鎖及膨脹流程圖

因爲自旋會消耗CPU,爲了避免無用的自旋(比如獲得鎖的線程被阻塞住了),一旦鎖升級
成重量級鎖,就不會再恢復到輕量級鎖狀態。當鎖處於這個狀態下,其他線程試圖獲取鎖時,
都會被阻塞住,當持有鎖的線程釋放鎖之後會喚醒這些線程,被喚醒的線程就會進行新一輪
的奪鎖之爭。

鎖對比

優點 缺點 適用場景
偏向鎖 加鎖和解鎖不需要額外的消耗,和執行非同步方法比僅存在納秒級的差距。 如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗。 適用於只有一個線程訪問同步塊場景。
輕量級鎖 競爭的線程不會阻塞,提高了程序的響應速度。 如果始終得不到鎖競爭的線程使用自旋會消耗CPU。 追求響應時間。同步塊執行速度非常快。
重量級鎖 線程競爭不使用自旋,不會消耗CPU。 線程阻塞,響應時間緩慢。 追求吞吐量。同步塊執行速度較長。

參考
方騰飛<Java併發編程的藝術>
周志明<深入理解Java虛擬機>

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章