你是哪家的鎖,這麼膨脹

指北君發表於2021-08-30

大家好,我是指北君。

在面試的時候,最會被問到的多執行緒問題就是 synchronized,如果還只會回答 monitorenter 和 monitorexit 那就有可能通不過面試,除了 monitorenter,還可以和麵試官聊聊 synchronized 的鎖膨脹。

初識

synchronized 可以加在方法和類上面,作用於類和物件。下面程式碼中列出了 synchronized 的用法。

public class SynchronizedTest {

    public static final Object lock = new Object();

    // 鎖的是SynchronizedTest.class物件
    public static synchronized void sync1() {

    }

    // 鎖的是SynchronizedTest.class物件
    public static void sync2() {
        synchronized (SynchronizedTest.class) {

        }
    }

    // 鎖的是當前例項this
    public synchronized void sync3() {

    }

    // 鎖的是當前例項this
    public void sync4() {
        synchronized (this) {

        }
    }

    // 鎖的是指定物件lock
    public void sync5() {
        synchronized (lock) {

        }
    }
}

synchronized 大家都知道是用 monitorenter 和 monitorexit 兩個指令鎖住同步塊的。

那麼 synchronized 是怎麼膨脹的呢?為什麼會膨脹呢?

先從 JVM 記憶體開始講起,物件在被例項化後,是存放在堆記憶體中的,它由 3 部分組成:

  1. 物件頭:存放物件執行時的狀態的資訊、指向該物件所屬 Class 的後設資料的指標。
  2. 例項資料:存放物件的屬性資料資訊,包括父類的資訊。
  3. 對齊填充位元組:由於虛擬機器要求物件的大小必須是 8 位元組的整數倍。不是必須存在,僅僅是為了位元組對齊。

其中物件頭裡麵包含了 Mark Word(標記欄位)和 Class Pointer(型別指標)

  1. Mark Word 預設的儲存物件的 hashcode、分代年齡、是否偏向鎖、鎖標識位的資訊,它在執行期間的儲存內容會隨著鎖的變化而變化。
Mark Word (32 bits) 是否偏向鎖 鎖標識位值 鎖狀態
物件的hashcode(25)、分代年齡(4)、是否偏向鎖(1)、鎖標識位(2) 0 01 無鎖
執行緒ID(23)、偏向時間戳(2)、分代年齡(4)、是否偏向鎖(1)、鎖標識位(2) 1 01 偏向鎖
指向棧中鎖記錄的指標(30)、鎖標識位(2) 00 輕量級鎖
指向重量級鎖的指標(30)、鎖標識位(2) 10 重量級鎖
  1. Class Pointer(型別指標):物件指向類的後設資料的指標,虛擬機器通過這個指標來確定物件是哪一個類的例項。

鎖膨脹

偏向鎖、輕量級鎖、重量級鎖、自旋鎖,這些都是Synchronzied的鎖的實現。Synchrozied會根據不同的場景選擇不同的鎖,我們只使用Synchronzied,不用關心它具體使用的哪個鎖。

偏向鎖

在java 程式中,大多數情況不存在多個執行緒同時競爭鎖,往往都是同一個執行緒多次獲得同一個鎖。

當只有一個執行緒在競爭鎖的時候,線上程獲取到鎖後,將進入偏向模式,程式會將物件的頭的前 23 個位元組用 CAS 的方式儲存執行緒 ID。下次有執行緒競爭鎖,只需要比較物件頭中的執行緒 ID 是不是和此時獲取到鎖的執行緒 ID 相同。如果相同執行緒就直接進入同步程式碼塊,不需要 CAS 競爭鎖。

有另外的執行緒在競爭鎖的時候,持有偏向鎖的執行緒才會釋放鎖,持有偏向鎖的執行緒不會主動釋放偏向鎖。
偏向鎖的撤銷,是在沒有位元組碼執行的時候進行的。首先會暫停偏向鎖的執行緒,判斷鎖物件是否被鎖住。撤銷偏向鎖後恢復成無鎖或者是輕量級鎖。

輕量級鎖

當有另外的執行緒在競爭偏向鎖的時候並且競爭失敗了,偏向鎖就會膨脹為輕量級鎖,其他的執行緒會通過自旋的方式嘗試獲取鎖。

JVM 會在當前執行緒的棧幀中建立一個叫做鎖記錄(Lock Record)的空間,將鎖物件的 Mark Word 複製進去。這個官方稱為 Displaced Mard Word。然後 JVM 將使用 CAS 操作嘗試將鎖物件的Mark Word 更新為指向 Lock Record 的指標。如果更新成功,鎖標識位就成為 00,此時為輕量級鎖。

重量級鎖

從上面的表格中就指出重量級鎖的物件頭裡面儲存的是指向 monitor 的指標,那 monitor 是什麼呢?

monitor 又稱為管程,Java 中由 ObjectMonitor 實現。 當執行緒要將物件加鎖的時候,物件會建立一個monitor。

ObjectMonitor 主要的欄位有:

  1. owner:就是當前加鎖的執行緒
  2. waitSet:就是 owner的執行緒呼叫了 wait() 方法,就進入這個裡面
  3. entryList:加鎖失敗的執行緒阻塞在這個裡面
  4. recursions:鎖的重入次數
  5. count:用來記錄是不是有物件加鎖:0.當前物件沒有執行緒加鎖,1. 當前物件有執行緒加鎖

從輕量級鎖升級到重量級鎖的時候,物件頭 Mark Word 儲存已經變成了指向 Monitor 的指標。執行緒可以通過這個指標找到 ObjectMonitor,放入 entryList 等待重量級鎖釋放後競爭。entryList 中的執行緒 CAS 嘗試更新 count = 1,當更新成功後將 owner 設定為當前的執行緒。當 owner 的執行緒呼叫了 wait() 方法,執行緒就會釋放鎖,進入 waitSet 中。這個時候 count = 1,owner = null,entryList 的執行緒可以再次競爭鎖。

總結

  1. synchronized 不管是加在類上還是方法上,如果作用在類上,這個類的所有物件都是同一把鎖,
  2. 鎖膨脹時不可以降級的

我是指北君,操千曲而後曉聲,觀千劍而後識器。感謝各位人才的:點贊、收藏和評論,我們下期更精彩!

相關文章