【JavaSE】淺談偏向鎖、輕量級鎖和重量級鎖,如何獲取鎖,如何撤銷鎖。

馮某r發表於2018-11-20

一、java物件頭

  • 鎖的獲取和撤銷會關係到物件頭,所以先來看看物件頭。
  • Java物件頭Mark Word欄位存放內容:
    在這裡插入圖片描述
  • 根據競爭狀態的激烈程度,鎖會自動進行升級,鎖不能降級(為了提高鎖獲取)

一、偏向鎖

  • 由於老版本的內建鎖synchronized存在最大的問題:在存線上程競爭的情況下會出現執行緒的阻塞以及喚醒帶來的效能問題,這是一種互斥同步(阻塞同步)。
  • 大多數情況下,鎖不僅不存在多執行緒競爭,而且總是由同一個執行緒多次獲得。為了讓執行緒獲取鎖的開銷降低引入偏向鎖。
  • JDK1.6之後引入了偏向鎖的概念,偏向鎖是一種樂觀鎖(假設所有執行緒訪問共享資源時不會出現衝突)。

1.偏向鎖的獲取

  • 1.因為鎖標誌位為01的時候會儲存執行緒ID,當一個執行緒進入同步程式碼塊的時候判斷物件頭裡的Mark Word是否儲存著指向當前執行緒ID,如果有就讓當前執行緒獲得鎖。(這裡可以解釋,當同一個執行緒不斷重入偏向鎖的時候不需要進行CAS操作,只需要進行一個判斷就可以獲得鎖)最後執行步驟5。如果沒有,則進行步驟2的判斷。
  • 2.判斷Mark Word中偏向鎖標識是否設定為1(表示當前是偏向鎖), 如果是的話指向步驟3 ,否則執行步驟4
  • 3.嘗試使用CAS將物件頭的偏向鎖指向當前執行緒, 成功表示獲取偏向鎖成功, 則執行步驟5, 失敗則表示存在競爭, 偏向鎖要升級為輕量級鎖, 偏向鎖撤銷和升級的流程下面再進行說明
  • 4.此時表示需要CAS競爭鎖。
  • 5.執行同步程式碼塊

2.偏向鎖的撤銷或者升級

  • 偏向鎖可以膨脹升級為輕量級鎖,前提是有第二個執行緒試圖獲取此偏向鎖。
  • 當兩個執行緒試圖獲取同一個偏向鎖的時候,如下圖所示:
    在這裡插入圖片描述
  • 線上程1執行同步程式碼塊之前的步驟上面已經解釋了,下面從執行緒2嘗試獲取鎖開始:
  • 1.判斷Mark Word中是否有指向自己的執行緒ID,否。
  • 2.判斷當前鎖是否為偏向鎖,是。那麼執行緒2會用CAS替換來嘗試獲取鎖。 CAS替換Mark Word成功表示獲取偏向鎖成功, 這裡由於物件頭中Mark Word已經指向了執行緒1, 所以替換失敗, 需要進行撤銷操作。
  • 3.撤銷偏向鎖, 需要等待全域性安全點(safepoint),就是當前時間點上沒有位元組碼正在執行。
  • 4.撤銷的時候需要等執行緒1全部跑完run方法,然後暫停執行緒1,如果執行緒1已經終止了,則將鎖物件的物件頭設定為無鎖狀態(方便下一個執行緒進來)。如果物件1還未終止,恢復執行緒1,並將鎖升級為輕量級鎖,然後和執行緒2一起CAS競爭輕量級鎖。
  • 我個人認為,擁有偏向鎖的執行緒不會自動釋放鎖,第一個執行緒申請偏向鎖的時候會成功,第二個執行緒來申請偏向鎖的時候,不管第一個執行緒執行完畢還是未完畢,都要進行一次撤銷或者升級的操作,因為當時Mark Wrod中儲存的指標不是執行緒2,所以只有撤銷為無鎖狀態,執行緒2才能成為新的偏向鎖偏向的執行緒
  • 鎖的物件頭中偏向著執行緒1,因為它不知道執行緒1什麼時候來,所以一直偏向著,就算執行緒1已經死亡了。所以撤銷鎖的時候,先檢查物件頭所指向的執行緒是否存活,如果不存活,那麼偏向鎖撤銷為無鎖,如果存在,那麼執行緒1目前沒有拿著鎖而在幹別的事情,這樣鎖就在不同時間段被不同執行緒訪問了升級為輕量級鎖,執行緒2就拿到了鎖。
  • 下面是我理解的偏向鎖整個的流程圖:
    在這裡插入圖片描述

3.偏向鎖關閉

偏向鎖是預設開啟的,而且開始時間一般是比應用程式啟動慢幾秒,如果不想有這個延遲,那麼可以使用-XX:BiasedLockingStartUpDelay=0;
如果不想要偏向鎖,那麼可以通過-XX:-UseBiasedLocking = false來設定;

二、輕量級鎖

  • 輕量級鎖可以允許多個執行緒嘗試獲取同一個鎖,但是必須是不同時間段的。
  • 如果在同一時間有多個鎖同時競爭輕量級鎖,那麼輕量級鎖就會膨脹變為重量級鎖。
  • 先看一張流程圖再詳解:
    在這裡插入圖片描述

1.輕量級加鎖操作

  • 執行緒在執行同步塊之前,JVM會先在當前執行緒的棧楨中建立用於儲存鎖記錄的空間,並將物件頭中的Mark Word拷貝到鎖記錄中,官方稱為Displaced Mark Word。然後執行緒嘗試使用CAS將物件頭中的Mark Word替換為指向鎖記錄的指標。
  • 為什麼要拷貝mark word?
    其實很簡單,原因是為了不想在lock與unlock這種底層操作上再加同步。(我的理解是如果每個執行緒進來都不拷貝,直接對內容進行更改的話,可能會出錯
  • 在拷貝完object mark word之後,JVM做了一步交換指標的操作,即流程中第一個橙色矩形框內容所述。
  • 上述操作如下圖所示:
    在這裡插入圖片描述
  • 如果成功,當前執行緒獲得鎖,如果失敗,表示其他執行緒競爭鎖,當前執行緒便嘗試使用自旋來獲取鎖。
  • 如果執行緒嘗試獲取鎖的時候,輕量鎖正被其他執行緒佔有,那麼它就會修改markword,修改重量級鎖,表示該進入重量鎖了。
  • 等待輕量鎖的執行緒不會阻塞,它會一直自旋等待鎖,並如上所說修改markword。
    這就是自旋鎖,嘗試獲取鎖的執行緒,在沒有獲得鎖的時候,不被掛起,而轉而去執行一個空迴圈,即自旋。在若干個自旋後,如果還沒有獲得鎖,則才被掛起,獲得鎖,則執行程式碼。
  • 雖然自旋可以防止阻塞,節省從核心態到使用者態的開銷,但是如果長時間自旋,則會導致CPU長時間做一個同樣的無用迴圈操作。浪費CPU的資源。這時候引入了自適應自旋

自適應自旋

  • 此操作為了防止長時間的自旋,在自旋操作上加了一些限制條件。
  • 比如一開始給執行緒自旋的時間是10秒,如果執行緒在這個時間內獲得了鎖,那麼就認為這個執行緒比較容易獲得鎖,就會適當的加長它的自旋時間。
  • 如果這個執行緒在規定時間內沒有獲得到鎖,並且阻塞了。那麼就認為這個執行緒不容易獲得鎖,下次當這個執行緒進行自旋的時候會減少它的自旋時間

2.輕量級鎖解鎖操作

  • 輕量級解鎖時,會使用原子的CAS操作將Displaced Mark Word替換回到物件頭。
  • 如果成功,則表示沒有競爭發生。成功替換,等待下一個執行緒獲取鎖。
  • 如果失敗,表示當前鎖存在競爭(因為自旋失敗的執行緒已經將物件頭中的輕量級鎖00改變為了10),鎖就會膨脹成重量級鎖。
  • 因為自旋會消耗CPU,為了避免無用的自旋(比如獲得鎖的執行緒被阻塞住了),一旦鎖升級成重量級鎖,就不會再 恢復到輕量級鎖狀態。當鎖處於這個狀態下,其他執行緒試圖獲取鎖時,都會被阻塞住,當持有鎖的執行緒釋放鎖之後 會喚醒這些執行緒,被喚醒的執行緒就會進行新一輪的奪鎖之爭。

三、重量級鎖

  • 重量級鎖是JVM中為基礎的鎖實現。在這種狀態下,JVM虛擬機器會阻塞加鎖失敗的執行緒,並且在目標鎖被釋放的 時候,喚醒這些執行緒。
  • Java執行緒的阻塞以及喚醒,都是依靠作業系統來完成的。舉例來說,對於符合posix介面的作業系統(如macOS和絕大 部分的Linux),上述操作通過pthread的互斥鎖(mutex)來實現的。此外,這些操作將涉及系統呼叫,需要從作業系統 的使用者態切換至核心態,其開銷非常之大。
  • 為了儘量避免昂貴的執行緒阻塞、喚醒操作,JVM會線上程進入阻塞狀態之前,以及被喚醒之後競爭不到鎖的情況 下,進入自旋狀態,在處理器上空跑並且輪詢鎖是否被釋放。如果此時鎖恰好被釋放了,那麼當前執行緒便無須進入 阻塞狀態,而是直接獲得這把鎖。

四、總結

Java虛擬機器中synchronized關鍵字的實現,按照代價由高到低可以分為重量級鎖、輕量鎖和偏向鎖三種。

  • 重量級鎖會阻塞、喚醒請求加鎖的執行緒。它針對的是多個執行緒同時競爭同一把鎖的情況。JVM採用了自適 應自旋,來避免執行緒在面對非常小的synchronized程式碼塊時,仍會被阻塞、喚醒的情況。
  • 輕量級鎖採用CAS操作,將鎖物件的標記欄位替換為一個指標,指向當前執行緒棧上的一塊空間,儲存著鎖對 象原本的標記欄位。它針對的是多個執行緒在不同時間段申請同一把鎖的情況。
  • 偏向鎖只會在第一次請求時採用CAS操作,在鎖物件的標記欄位中記錄下當前執行緒的地址。在之後的執行過 程中,持有該偏向鎖的執行緒的加鎖操作將直接返回。它針對的是鎖僅會被同一執行緒持有的情況。

相關文章