1. 程式人生 > 其它 >synchronized鎖升級過程及驗證

synchronized鎖升級過程及驗證

synchronized鎖升級過程

其實“鎖”本身就是個物件,synchronized這個關鍵字不是鎖,而是在加上synchronized時,僅僅是相當於“加鎖”這個操作。

synchronized 是通過鎖物件來實現的。因此瞭解一個物件的佈局,對我們理解鎖的實現及升級是很有幫助的。

物件佈局

物件頭(Object Header)

在64位JVM上有一個壓縮指標選項-XX:+UseCompressedOops,預設是開啟的。開啟之後 Class Pointer 部分就會壓縮為4位元組,物件頭大小為 12 位元組

  • 物件頭

    • Mark Word

      • 預設儲存物件的HashCode,分代年齡和鎖標誌位資訊。
      • 這些資訊都是與物件自身定義無關的資料,所以Mark Word被設計成一個非固定的資料結構以便在極小的空間記憶體儲存儘量多的資料。
      • 它會根據物件的狀態複用自己的儲存空間,也就是說在執行期間Mark Word裡儲存的資料會隨著鎖標誌位的變化而變化。
      • 包含一系列的標記位,比如輕量級鎖的標記位,偏向鎖標記位等等。
    • Class Pointer

      • 物件指向它的類元資料的指標;
      • 虛擬機器通過這個指標來確定這個物件是哪個類的例項;
    • Length:如果是陣列物件,還有一個儲存陣列長度的空間,佔4個位元組;

  • 例項資料

    • 物件實際資料包括了物件的所有成員變數,其大小由各個成員變數的大小決定;
  • 對齊填充
    Java物件佔用空間是8位元組對齊的,即所有Java物件佔用bytes數必須是8的倍數。

例如,一個包含兩個屬性的物件:int和byte,這個物件需要佔用8+4+1=13個位元組,這時就需要加上大小為3位元組的padding進行8位元組對齊,最終佔用大小為16個位元組。

Mark Word

偏向鎖位鎖標誌位 是鎖升級過程中承擔重要的角色。

Jol 檢視物件資訊

我們可以使用 jol 檢視一個物件的物件頭資訊,已達到觀測鎖升級的過程

//依賴
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>

//輸出物件資訊
ClassLayout layout = ClassLayout.parseInstance(object)
System.out.println(layout.toPrintable());

普通物件到輕量級鎖

因為偏向鎖的延遲,建立的物件為普通物件(偏向鎖位 0,鎖標誌位 01),獲取鎖的時候,無鎖(偏向鎖位 0,鎖標誌位 01) 升級為 輕量級鎖(偏向鎖位 0,鎖標誌位 00),釋放鎖之後,物件的鎖資訊(偏向鎖位 0,鎖標誌位 01)

為什麼要延遲4s?

因為JVM虛擬機器自己有一些預設的啟動執行緒,裡面有好多sync程式碼,這些程式碼啟動時就肯定會有競爭,如果直接使用偏向鎖,就會造成偏向鎖不斷的進行鎖撤銷和鎖升級的操作,效率較低。

synchronized (a) 的時候,由 aMark Word 中鎖偏向 0,鎖標誌位 01 知道鎖要升級為輕量級鎖。java 虛擬機器會在當前的執行緒的棧幀中建立一個鎖記錄(Lock Record)空間,Lock Record 儲存鎖物件的 Mark World拷貝和當前鎖物件的指標。

java 虛擬機器,使用 CAS 將 a 的 Mark Word(62 位) 指向當前執行緒(main 執行緒)中 Lock Record 指標,CAS 操作成功,將 a 的鎖標誌位變為 00,升級為輕量級鎖。

輕量級鎖解鎖,就是將 Lock Record 中的 a 的 mark word 拷貝,通過 CAS 替換 a 物件頭中的 mark word ,替換成功解鎖順利完成。

import org.openjdk.jol.info.ClassLayout;

/**
 * <P><B>Description: </B> 普通物件升級到輕量級鎖  </P>
 */
public class Snychronized1 {
    public static class A {
    }
    public static void main(String[] args) throws Exception {
        A a = new A();
        ClassLayout layout = ClassLayout.parseInstance(a);
        System.out.println("**** 物件建立,沒有經過鎖競爭");
        System.out.println(layout.toPrintable());
        synchronized (a) {
            System.out.println("**** 獲取到鎖");
            System.out.println(layout.toPrintable());
        }
        System.out.println("**** 鎖釋放");
        System.out.println(layout.toPrintable());
    }


}

偏向鎖

偏向鎖是比輕量級鎖更輕量的鎖。輕量級鎖,每次獲取鎖的時候,都會使用 CAS 判斷是否可以加鎖,不管有沒有別的執行緒競爭。

當執行緒要進入synchronized修飾的方法或程式碼塊時,jvm會判斷物件頭中的MarkWord中有沒有偏向鎖指向當前執行緒ID,如果有,若此時無其他執行緒競爭,保持偏向鎖狀態。當該執行緒重複進入方法或程式碼塊時(重入),直接在MarkWord中判斷有沒有偏向鎖指向它的執行緒ID,就不用通過 CAS 操作獲取偏向鎖了。

當有其他執行緒加入競爭後,執行緒會暫停檢查,若果該執行緒執行完了,則撤銷鎖,其他執行緒佔有該鎖,如果該執行緒還未執行完還需要該鎖,則將鎖升級為輕量級鎖。


import org.openjdk.jol.info.ClassLayout;

import java.util.concurrent.TimeUnit;

/**
 * <P><B>Description: </B> 偏向鎖  </P>
 */

// 檢視偏向鎖配置的預設引數  -XX:+PrintFlagsInitial | grep -i biased
// BiasedLocking
public class Snychronized2 {
    public static class A {

    }

    public static void main(String[] args) throws Exception {
        //因為偏向鎖加鎖機制延遲4秒啟動,所以我們這裡阻塞6s再建立物件。
        TimeUnit.SECONDS.sleep(6);
        final A a = new A();
        ClassLayout layout = ClassLayout.parseInstance(a);
        System.out.println("**** 建立物件,物件獲得偏向鎖");
        System.out.println(layout.toPrintable());

        synchronized (a) {
            System.out.println("**** 沒有其他執行緒競爭,依舊保持偏向鎖");
            System.out.println(layout.toPrintable());
        }

        System.out.println("**** 解鎖後,物件還是持有偏向鎖");
        System.out.println(layout.toPrintable());

    }


}

輕量級鎖

就是偏向鎖升級來的。該執行緒還未執行完,繼續佔有資源,其他執行緒等待,這是其他執行緒就會自旋,等待資源釋放。若自旋解鎖失敗,鎖升級為重量級鎖。

重量級鎖

驗證 偏向鎖,輕量級鎖,重量級鎖的逐漸升級過程。

/**
 * <P><B>Description: </B> 偏向鎖,輕量級鎖,重量級鎖的逐漸升級  </P>
 */
public class Snychronized3 {
    public static void main(String[] args) throws Exception {
        // 延遲六秒執行例子,建立的 a 為可偏向物件
        TimeUnit.SECONDS.sleep(6);
        final A a = new A();
        ClassLayout layout = ClassLayout.parseInstance(a);
        System.out.println("**** 檢視初始化 a 的物件頭");
        System.out.println(layout.toPrintable());
        // 這裡模擬獲取鎖,當前獲取到的鎖為 偏向鎖
        Thread t = new Thread(() -> {
            synchronized (a) {
            }
        });
        t.start();
        // 阻塞等待獲取 t 執行緒完成
        t.join();
        System.out.println("**** t 執行緒獲得鎖之後");
        System.out.println(layout.toPrintable());

        final Thread t2 = new Thread(() -> {
            synchronized (a) {
                // a 的存在兩個想成競爭鎖,偏向鎖升級為輕量級鎖
                System.out.println("**** t2 第二次獲取鎖");
                System.out.println(layout.toPrintable());
                try {
                    //阻塞3秒,模擬任務執行
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 開啟 t3 執行緒模擬競爭,t3 會自旋獲得鎖,由於 t2 阻塞了 3 秒,t3 自旋是得不到鎖的,鎖升級為重量級鎖
        final Thread t3 = new Thread(() -> {
            synchronized (a) {
                System.out.println("**** t3 不停獲取鎖");
                System.out.println(layout.toPrintable());
            }
        });
        t2.start();
        // 為了保證 t2 先獲得鎖,這裡阻塞 10ms ,先開啟t2執行緒,再開啟 t3 執行緒
        TimeUnit.MILLISECONDS.sleep(10);
        t3.start();
        t2.join();t3.join();

        // 驗證 gc 可以使鎖降級
        System.gc();
        System.out.println("**** After System.gc()");
        System.out.println(layout.toPrintable());
    }
    public static class A {}
}

t2 執行緒持有鎖 a輕量級鎖 的時候,t3 也在獲得 a 的 輕量級鎖CAS 修改 a 的 Mark Word 為 t3 所有失敗。導致了鎖升級為重量級鎖,設定 a 的鎖標誌位為 10,並且將 Mark Word 指標指向一個 monitor物件,並將當前執行緒阻塞,將當前執行緒放入到 _EntryList 佇列中。當 t2 執行完之後,它解鎖的時候發現當前鎖已經升級為重量級鎖,釋放鎖的時候,會喚醒 _EntryList 的執行緒,讓它們去搶 a 鎖。

自旋,底層其實呼叫的是native方法,涉及到彙編相關的問題,說白了就是為了保持執行緒不進入睡眠狀態,讓cpu做無用功。其實自旋最大的一個作用就是避免了執行緒在使用者態和核心態之間切換。減少cpu資源的排程消耗,但是也不能一直自旋,不然另一個執行緒一直佔用著鎖,而你在這一直自旋消耗cpu資源,導致cpu佔用率一路飆升也不行,所以jvm有設定最大自旋次數,10次。

到底什麼時候鎖會降級呢?

正常情況下,是不會發生鎖降級的,鎖降級一般只會發生在GC的時候,GC的時候,物件都即將被回收,沒用了,所以說鎖降級沒什麼太大的意義。

jdk1.6 其他優化:

鎖消除:JIT編譯時,檢測到共享資料區存在不可能出現競爭情況,就會進行鎖消除。例如同步方法內的區域性變數,不可能被其他執行緒使用,就會進行鎖消除

虛擬機器預設開啟了鎖消除 -XX:-EliminateLocks 關閉鎖消除

鎖粗化:把多次鎖請求合併成一個鎖請求,降低效能消耗。

/**
 * <P><B>Description: </B> 鎖消除,鎖粗化  </P>
 */
public class SynchronizedTest {


/*    @Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }*/

// 從原始碼中可以看出,append方法用了synchronized關鍵詞,它是執行緒安全的。
// 但我們可能僅線上程內部把StringBuffer當作區域性變數使用,
// 這時候,編譯器就會判斷出sb這個物件並不會被這段程式碼塊以外的地方訪問到,
// 更不會被其他執行緒訪問到,這時候的加鎖就是完全沒必要的,編譯器就會把這裡的加鎖程式碼消除掉,
// 體現到java原始碼上就是把append方法的synchronized修飾符給去掉了。


    public static String getString(String s1, String s2) {
        StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        return sb.toString();
    }


    //鎖消除測試
    public static void test1() {

        long tsStart = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            getString("TestLockEliminate ", "Suffix");
        }
        System.out.println("一共耗費:" + (System.currentTimeMillis() - tsStart) + " ms");
    }


    //鎖粗化測試
    public static void test2() {
        long tsStart = System.currentTimeMillis();
        Object object = new Object();
         synchronized (object) {
        for (int i = 0; i < 100000000; i++) {
           // synchronized (object) {
                object.hashCode();

            }
        }
        System.out.println("一共耗費:" + (System.currentTimeMillis() - tsStart) + " ms");
    }

    public static void main(String[] args) {
        //鎖消除測試
        //   -XX:-EliminateLocks 關閉鎖消除
        //test1();

        //鎖粗化測試
        test2();

    }
}