【轉】談談 JVM 內部鎖升級過程

一、加鎖發生了什麼

//System.out.println都加了鎖
public void println(String x) {
  synchronized (this) {
    print(x);
    newLine();
  }
}

簡單加鎖發生了什麼?

要弄清楚加鎖之後到底發生了什麼需要看一下對象創建之後再內存中的佈局是個什麼樣的?

一個對象在 new 出來之後在內存中主要分爲 4 個部分:

  • Markword 這部分其實就是加鎖的核心,同時還包含的對象的一些生命信息,例如是否 GC、進過了幾次 Young GC 還存活等。
  • klass pointer 記錄了指向對象的 class 文件指針。
  • instance data 記錄了對象裏面的變量數據。
  • padding 作爲對齊使用,對象在 64 位服務器版本中,規定對象內存必須要能被 8 字節整除,如果不能整除,那麼就靠對齊來補。舉個例子:new 出了一個對象,內存只佔用 18 字節,但是規定要能被 8 整除,所以 padding=6。

知道了這 4 個部分之後,我們來驗證一下底層。藉助於第三方包 JOL = Java Object Layout java 內存佈局去看看。很簡單的幾行代碼就可以看到內存佈局的樣式:

<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>
public class JOLDemo {
    private static Object  o;
    public static void main(String[] args) {
        o = new Object();
        synchronized (o){
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}

將結果打印出來:

從輸出結果看:

  1. 對象頭包含了 12 個字節分爲 3 行,其中前 2 行其實就是 Markword,第三行就是 klass 指針。值得注意的是在加鎖前後輸出從 001 變成了 000。Markword 用處:8 字節(64bit)的頭記錄一些信息,鎖就是修改了 Markword 的內容 8 字節(64bit)的頭記錄一些信息,鎖就是修改了markword的內容字節(64bit)的頭記錄一些信息。從 001 無鎖狀態,變成了 00 輕量級鎖狀態。

  2. new 出一個 object 對象,佔用 16 個字節。對象頭佔用 12 字節,由於 Object 中沒有額外的變量,所以 instance = 0,考慮要對象內存大小要被 8 字節整除,那麼 padding=4,最 後 new Object() 內存大小爲 16 字節。

二、鎖的升級過程

2.1 鎖的升級驗證

探討鎖的升級之前,先做個實驗。兩份代碼,不同之處在於一箇中途讓它睡了5秒,一個沒睡。看看是否有區別。

public class JOLDemo {
    private static Object  o;
    public static void main(String[] args) {
        o = new Object();
        synchronized (o){
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}
----------------------------------------------------------------------------------------------
public class JOLDemo {
    private static Object  o;
    public static void main(String[] args) {
      try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); }
        o = new Object();
        synchronized (o){
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
}

這兩份代碼會不會有什麼區別?運行之後看看結果:

有點意思的是,讓主線程睡了 5s 之後輸出的內存佈局跟沒睡的輸出結果居然不一樣。Syn 鎖升級之後,jdk1.8 版本的一個底層默認設置 4s 之後偏向鎖開啓。也就是說在 4s 內是沒有開啓偏向鎖的,加了鎖就直接升級爲輕量級鎖了。

那麼這裏就有幾個問題了?

  • 爲什麼要進行鎖升級,以前不是默認 syn 就是重量級鎖麼?要麼不用要麼就用別的不行麼?
  • 既然 4s 內如果加了鎖就直接到輕量級,那麼能不能不要偏向鎖,爲什麼要有偏向鎖?
  • 爲什麼要設置 4s 之後開始偏向鎖?

問題 1:爲什麼要進行鎖升級?鎖了就鎖了,不就要加鎖麼?

首先明確 syn 鎖 在 jdk1.2 之前效率非常低。那時候 syn 就是重量級鎖,申請鎖必須要經過操作系統老大 kernel 進行系統調用,入隊進行排序操作,操作完之後再返回給用戶態。

內核態:用戶態如果要做一些比較危險的操作直接訪問硬件,很容易把硬件搞死(格式化,訪問網卡,訪問內存幹掉等),操作系統爲了系統安全分成兩層:用戶態和內核態。申請鎖資源的時候用戶態要向操作系統老大內核態申請。Jdk1.2 的時候用戶需要跟內核態申請鎖,然後內核態還會給用戶態。這個過程是非常消耗時間的,導致早期效率特別低。有些 jvm 就可以處理的爲什麼還交給操作系統做去呢?能不能把 jvm 就可以完成的鎖操作拉取出來提升效率,所以也就有了鎖優化。

問題 2:爲什麼要有偏向鎖?

其實這本質上歸根於一個概率問題,統計表示,在我們日常用的 syn 鎖過程中 70%-80% 的情況下,一般都只有一個線程去拿鎖,例如我們常使用的 System.out.println、StringBuffer,雖然底層加了 syn 鎖,但是基本沒有多線程競爭的情況。那麼這種情況下,沒有必要升級到輕量級鎖級別了。

偏向的意義在於:第一個線程拿到鎖,將自己的線程信息標記在鎖上,下次進來就不需要在拿去拿鎖驗證了。如果超過 1 個線程去搶鎖,那麼偏向鎖就會撤銷,升級爲輕量級鎖,其實我認爲嚴格意義上來講偏向鎖並不算一把真正的鎖,因爲只有一個線程去訪問共享資源的時候纔會有偏向鎖這個情況。

問題 3:爲什麼 jdk8 要在 4s 後開啓偏向鎖?

其實這是一個妥協,明確知道在剛開始執行代碼時,一定有好多線程來搶鎖,如果開了偏向鎖效率反而降低,所以上面程序在睡了 5s 之後偏向鎖纔開放。爲什麼加偏向鎖效率會降低,因爲中途多了幾個額外的過程,上了偏向鎖之後多個線程爭搶共享資源的時候要進行鎖升級到輕量級鎖,這個過程還的把偏向鎖進行撤銷在進行升級,所以導致效率會降低。爲什麼是 4s?這是一個統計的時間值。

當然我們是可以禁止偏向鎖的,通過配置參數 -XX:-UseBiasedLocking = false 來禁用偏向鎖。jdk15 之後默認已經禁用了偏向鎖。本文是在 jdk8 的環境下做的鎖升級驗證。

2.2 鎖的升級流程

上面已經驗證了對象從創建出來之後進內存從無鎖狀態->偏向鎖(如果開啓了)->輕量級鎖的過程。對於鎖升級的流程繼續往下,輕量級鎖之後就會變成重量級鎖。首先我們先理解什麼叫做輕量級鎖,從一個線程搶佔資源(偏向鎖)到多線程搶佔資源升級爲輕量級鎖,線程如果沒那麼多的話,其實這裏就可以理解爲 CAS(Compare and Swap:比較並交換值)。

問題 4:什麼情況下輕量級鎖要升級爲重量級鎖呢?

首先我們可以思考的是多個線程的時候先開啓輕量級鎖,如果它 carry 不了的情況下才會升級爲重量級。那麼什麼情況下輕量級鎖會 carry 不住?

  1. 如果線程數太多,比如上來就是 10000 個,那麼這裏 CAS 要轉多久纔可能交換值,同時 CPU 光在這 10000 個活着的線程中來回切換中就耗費了巨大的資源,這種情況下自然就升級爲重量級鎖,直接叫給操作系統入隊管理,那麼就算 10000 個線程那也是處理休眠的情況等待排隊喚醒。
  2. CAS 如果自旋 10 次依然沒有獲取到鎖,那麼也會升級爲重量級。

總的來說,兩種情況都會從輕量級升級爲重量級,10 次自旋或等待 cpu 調度的線程數超過 cpu 核數的一半,自動升級爲重量級鎖。整個鎖升級過程如圖所示:

問題 5:都說 syn 爲重量級鎖,那麼到底重在哪裏?

JVM 偷懶把任何跟線程有關的操作全部交給操作系統去做,例如調度鎖的同步直接交給操作系統去執行,而在操作系統中要執行先要入隊,另外操作系統啓動一個線程時需要消耗很多資源,消耗資源比較重,重就重在這裏。




原文鏈接:

談談JVM內部鎖升級過程

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