盤一盤 synchronized (二)—— 偏向鎖批量重偏向與批量撤銷

檸檬五個半發表於2019-07-26
在本文講解之前,先來簡單瞭解一下為什麼會有批量重偏向和批量撤銷。
 
批量重偏向:當一個執行緒建立了大量物件並執行了初始的同步操作,後來另一個執行緒也來將這些物件作為鎖物件進行操作,會導偏向鎖重偏向的操作。
批量撤銷:在多執行緒競爭劇烈的情況下,使用偏向鎖將會降低效率,於是乎產生了批量撤銷機制。
 

 JVM的預設引數值

通過JVM的預設引數值,找一找批量重偏向和批量撤銷的閾值。
設定JVM引數-XX:+PrintFlagsFinal,在專案啟動時即可輸出JVM的預設引數值
 
intx BiasedLockingBulkRebiasThreshold   = 20   預設偏向鎖批量重偏向閾值
intx BiasedLockingBulkRevokeThreshold  = 40   預設偏向鎖批量撤銷閾值
當然我們可以通過-XX:BiasedLockingBulkRebiasThreshold 和 -XX:BiasedLockingBulkRevokeThreshold 來手動設定閾值

 

批量重偏向

public static void main(String[] args) throws Exception {
        //延時產生可偏向物件
        Thread.sleep(5000);

        //創造100個偏向執行緒t1的偏向鎖
        List<A> listA = new ArrayList<>();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i <100 ; i++) {
                A a = new A();
                synchronized (a){
                    listA.add(a);
                }
            }
            try {
                //為了防止JVM執行緒複用,在建立完物件後,保持執行緒t1狀態為存活
                Thread.sleep(100000000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t1.start();

        //睡眠3s鍾保證執行緒t1建立物件完成
        Thread.sleep(3000);
        out.println("列印t1執行緒,list中第20個物件的物件頭:");
        out.println((ClassLayout.parseInstance(listA.get(19)).toPrintable()));

        //建立執行緒t2競爭執行緒t1中已經退出同步塊的鎖
        Thread t2 = new Thread(() -> {
            //這裡面只迴圈了30次!!!
            for (int i = 0; i < 30; i++) {
                A a =listA.get(i);
                synchronized (a){
                    //分別列印第19次和第20次偏向鎖重偏向結果
                    if(i==18||i==19){
                        out.println("第"+ ( i + 1) + "次偏向結果");
                        out.println((ClassLayout.parseInstance(a).toPrintable()));
                    }
                }
            }
            try {
                Thread.sleep(10000000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t2.start();

        Thread.sleep(3000);
        out.println("列印list中第11個物件的物件頭:");
        out.println((ClassLayout.parseInstance(listA.get(10)).toPrintable()));
        out.println("列印list中第26個物件的物件頭:");
        out.println((ClassLayout.parseInstance(listA.get(25)).toPrintable()));
        out.println("列印list中第41個物件的物件頭:");
        out.println((ClassLayout.parseInstance(listA.get(40)).toPrintable()));
    }
 
我們一步一步來分析列印結果
首先,創造了100個偏向執行緒t1的偏向鎖,結果沒什麼好說的,100個偏向鎖嘛,偏向的執行緒ID資訊為531939333

 

再來看看重偏向的結果,執行緒t2,前19次偏向均產生了輕量鎖,而到第20次的時候,達到了批量重偏向的閾值20,此時鎖並不是輕量級鎖,而變成了偏向鎖,此時偏向的執行緒t2,執行緒t2的ID資訊為5322162821

 

最後我們再來看一下偏向結束後的物件頭資訊。
前20個物件,並沒有觸發了批量重偏向機制,執行緒t2執行釋放同步鎖後,轉變為無鎖形態
第20~30個物件,觸發了批量重偏向機制,物件為偏向鎖狀態,偏向執行緒t2,執行緒t2的ID資訊為5322162821
而31個物件之後,也沒有觸發了批量重偏向機制,物件仍偏向執行緒t1,執行緒t1的ID資訊為531939333

 

批量撤銷

 
我們再來看看批量撤銷
public static void main(String[] args) throws Exception {

    Thread.sleep(5000);
    List<A> listA = new ArrayList<>();

    Thread t1 = new Thread(() -> {
        for (int i = 0; i <100 ; i++) {
            A a = new A();
            synchronized (a){
                listA.add(a);
            }
        }
        try {
            Thread.sleep(100000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    t1.start();
    Thread.sleep(3000);

    Thread t2 = new Thread(() -> {
        //這裡迴圈了40次。達到了批量撤銷的閾值
        for (int i = 0; i < 40; i++) {
            A a =listA.get(i);
            synchronized (a){
            }
        }
        try {
            Thread.sleep(10000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    t2.start();

    //———————————分割線,前面程式碼不再贅述——————————————————————————————————————————
    Thread.sleep(3000);
    out.println("列印list中第11個物件的物件頭:");
    out.println((ClassLayout.parseInstance(listA.get(10)).toPrintable()));
    out.println("列印list中第26個物件的物件頭:");
    out.println((ClassLayout.parseInstance(listA.get(25)).toPrintable()));
    out.println("列印list中第90個物件的物件頭:");
    out.println((ClassLayout.parseInstance(listA.get(89)).toPrintable()));


    Thread t3 = new Thread(() -> {
        for (int i = 20; i < 40; i++) {
            A a =listA.get(i);
            synchronized (a){
                if(i==20||i==22){
                    out.println("thread3 第"+ i + "次");
                    out.println((ClassLayout.parseInstance(a).toPrintable()));
                }
            }
        }
    });
    t3.start();


    Thread.sleep(10000);
    out.println("重新輸出新例項A");
    out.println((ClassLayout.parseInstance(new A()).toPrintable()));
}
 
來看看輸出結果,這部分和上面批量偏向結果的大相徑庭。重點關注記錄的執行緒ID資訊
前20個物件,並沒有觸發了批量重偏向機制,執行緒t2執行釋放同步鎖後,轉變為無鎖形態
第20~40個物件,觸發了批量重偏向機制,物件為偏向鎖狀態,偏向執行緒t2,執行緒t2的ID資訊為540039429
而41個物件之後,也沒有觸發了批量重偏向機制,物件仍偏向執行緒t1,執行緒t1的ID資訊為540002309

 

再來看看最後新生成的物件A。值得注意的是:本應該為可偏向狀態偏向鎖的新物件,在經歷過批量重偏向和批量撤銷後直接在例項化後轉為無鎖。

 

簡單總結

1、批量重偏向和批量撤銷是針對類的優化,和物件無關。
2、偏向鎖重偏向一次之後不可再次重偏向。
3、當某個類已經觸發批量撤銷機制後,JVM會預設當前類產生了嚴重的問題,剝奪了該類的新例項物件使用偏向

相關文章