synchronized鎖升級過程及驗證


synchronized鎖升級過程

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

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

對象布局

image

對象頭(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

image

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

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());

image

image

普通對象到輕量級鎖

因為偏向鎖的延遲,創建的對象為普通對象(偏向鎖位 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();

    }
}


免責聲明!

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



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