【轉】談談 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內部鎖升級過程


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM