1. 程式人生 > 實用技巧 >關於synchronized批量重偏向和批量撤銷的一個小實驗

關於synchronized批量重偏向和批量撤銷的一個小實驗

前段時間學習synchronized的時候做過一個關於批量重偏向和批量撤銷的小實驗,感覺挺有意思的,所以想分享一下。雖然是比較底層的東西,但是結論可以通過做實驗看出來,就挺有意思。

我們都知道synchronized分為偏向鎖、輕量級鎖和重量級鎖這三種,這個實驗主要是和偏向鎖相關的。關於偏向鎖,我們又知道,偏向鎖在偏向了某一個執行緒之後,不會主動釋放鎖,只有出現競爭了才會執行偏向鎖撤銷。

先說結論吧,開啟偏向鎖時,在「規定的時間」內,如果偏向鎖撤銷的次數達到20次,就會執行批量重偏向,如果撤銷次數達到了40次,就會觸發批量撤銷。批量重偏向和批量撤銷都可以理解為虛擬機器的一種優化機制,我理解主要是出於效能上的考慮。

當一個鎖物件類的撤銷次數達到20次時,虛擬機器會認為這個鎖不適合再偏向於原執行緒,於是會在偏向鎖撤銷達到20次時讓這一類鎖嘗試偏向於其他執行緒。

當一個鎖物件類的撤銷次數達到40次時,虛擬機器會認為這個鎖根本就不適合作為偏向鎖使用,因此會將類的偏向標記關閉,之後現存物件加鎖時會升級為輕量級鎖,鎖定中的偏向鎖物件會被撤銷,新建立的物件預設為無鎖狀態。


下面先說明一下實驗之前要準備的東西。首先,我們得開啟偏向鎖,關閉偏向鎖的延遲啟動。由於只有看到物件頭的鎖標誌位才能判斷鎖的型別,因此還需要用到OpenJDK提供的JOL(Java Object Layout)包。由於下面講解涉及到物件頭的mark word,這裡貼一張mark word的說明圖。epoch可以理解為鎖物件的年齡標記,利用JOL檢視物件頭時主要關注鎖標誌位即可。

鎖物件mark word的低三位為001時,表示無鎖且不可偏向,若為101則表示匿名偏向或偏向鎖定狀態(取決於是否有Thread ID)。鎖物件類也有鎖標誌位的概念,作用和鎖物件類似,我理解只是作用範圍的區別。鎖物件類若為不可偏向,所有新建立的物件都是不可偏向的。

實驗程式碼包括39個鎖物件和3個執行緒:T1、T2、T3,分三次執行加鎖解鎖操作,因此鎖物件的狀態的變化也分為三個階段。

第一階段是T1執行緒執行。T1執行緒執行後,由於是第一次加鎖,因此所有物件都偏向於T1。

此時從物件頭mark word可以看出,所有物件都處於偏向鎖定狀態(偏向於T1)。

05 70 51 19 (0000010101110000 01010001 00011001) (424767493)

00 00 00 00 (00000000 00000000 00000000 00000000) (0)

第二階段是T2執行緒執行。T2執行緒執行後,0~18號物件會執行偏向鎖撤銷,鎖物件狀態變化為:偏向鎖->輕量級鎖->無鎖。偏向鎖撤銷執行到19號物件,也就是第20個鎖物件時,會觸發批量重偏向,此時19~38號物件會批量重偏向於T2。實際上此時只會修改類物件的epoch和處於加鎖中的鎖物件的epoch(也就是說不會重偏向處於使用中的鎖物件),其他未處於加鎖中的鎖物件的重偏向則發生於下一次加鎖時,判斷條件是類物件epoch和鎖物件epoch是否一致,不一致則會執行重偏向。T2退出同步程式碼後的最終結果就是0~18號物件變為無鎖狀態,19~38號物件偏向於T2,偏向鎖撤銷次數為20次。

此時從物件頭mark word可以看出,0~18號物件處於無鎖狀態,19~38號物件則處於偏向鎖定狀態(偏向於T2)。

0~18號物件:

01 00 00 00 (0000000100000000 00000000 00000000) (1)

00 00 00 00 (00000000 00000000 00000000 00000000) (0)

19~38號物件:

05 99 51 19 (0000010110011001 01010001 00011001) (424777989)

00 00 00 00 (00000000 00000000 00000000 00000000) (0)

第三階段是T3執行緒執行。此時0~18已經處於無鎖狀態,只能加輕量級鎖。19~38號物件則有所不同,這20個物件執行時會逐個執行偏向鎖撤銷,到第38號物件時剛好又執行了20次,此時總的撤銷次數到達40次,於是觸發批量撤銷。批量撤銷會將類的偏向標記關閉,之後現存物件加鎖時會升級為輕量級鎖,鎖定中的偏向鎖物件會被撤銷,新建立的物件預設為無鎖狀態。

此時從物件頭mark word可以看出,0~37號物件處於無鎖狀態,38號物件也處於無鎖狀態(升級成輕量級鎖後又解鎖了)。

01 00 00 00 (0000000100000000 00000000 00000000) (1)

00 00 00 00 (00000000 00000000 00000000 00000000) (0)


以上步驟是常規步驟,如果把「sleep 30s」部分的註釋程式碼放開,事情就不一樣了。

虛擬機器的偏向鎖實現裡有兩個很關鍵的東西:BiasedLockingDecayTime和revocation_count。

BiasedLockingDecayTime是開啟一次新的批量重偏向距離上一次批量重偏向之後的延遲時間,預設為25000ms,這就是上面講到的「規定的時間」。revocation_count是撤銷計數器,會記錄偏向鎖撤銷的次數。也就是說,在執行一次批量重偏向之後,經過了較長的一段時間(>=BiasedLockingDecayTime)之後,撤銷計數器才超過閾值,則會重置撤銷計數器。而是否執行批量重偏向和批量撤銷正是依賴於撤銷計數器的,sleep之後計數器被清零,本次不執行批量撤銷,因此後續也就有機會繼續執行批量重偏向。

根據以上知識可知,等待一段時間後撤銷計數器會清零,因此不會再執行批量撤銷,而是變成再次執行批量重偏向。此時T3加鎖的過程就和上面有所不同了,0~18號物件已經變為無鎖,因此這部分只能加輕量級鎖。關鍵是19~38號物件,從19號物件開始又會執行偏向鎖撤銷,到38號物件時剛好20次,這就繞回常規情況下T2執行時的場景了,T2執行時19號物件是不是從偏向T1變成了偏向T2?所以這裡從38號物件開始往後的其他物件都會從T2重新偏向T3。

這裡的特性用虛擬機器裡面的話講叫做「啟發式更新」,我理解這樣做主要是出於效能上的考慮。假如偏向鎖只是偶爾會發生輪流加鎖的這種競爭,虛擬機器是允許的,20次以內隨便你怎麼玩,可以一直幫你執行偏向鎖撤銷。如果25秒內撤銷次數超過20次了,還友情提供一次批量重偏向。但是假如執行緒間競爭很多,頻繁執行偏向鎖撤銷和批量重偏向則可能會比較損耗效能,因此「規定的時間」內連續撤銷超過一定次數(預設40次)虛擬機器就不讓你偏向了,這就是批量撤銷的意義所在。

大概就這些。

實驗程式碼:

import org.openjdk.jol.info.ClassLayout;

import java.util.Vector;
import java.util.concurrent.locks.LockSupport;

/**
 * -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 -XX:+PrintCommandLineFlags
 */
public class TestBiased {

    static Thread T1, T2, T3;

    public static void main(String[] args) throws InterruptedException {
        test();
    }

    private static void test() throws InterruptedException {
        Vector<Doge> list = new Vector<>();

        int loopNumber = 39;
        T1 = new Thread(() -> {
            for (int i = 0; i < loopNumber; i++) {
                Doge d = new Doge();
                list.add(d);
                synchronized (d) {
                    System.out.println(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
                }
            }
            LockSupport.unpark(T2);
        }, "T1");
        T1.start();

        T2 = new Thread(() -> {
            LockSupport.park();
            System.out.println("===============> ");
            for (int i = 0; i < loopNumber; i++) {
                Doge d = list.get(i);
                System.out.println(i + "\t" + ClassLayout.parseInstance(d).toPrintable());

                synchronized (d) {
                    System.out.println(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
                }
                System.out.println(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
            }

            //sleep 30s
            /*try {
                Thread.sleep(30000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }*/

            LockSupport.unpark(T3);
        }, "T2");
        T2.start();

        T3 = new Thread(() -> {
            LockSupport.park();
            System.out.println("===============> ");
            for (int i = 0; i < loopNumber; i++) {
                Doge d = list.get(i);
                System.out.println(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
                synchronized (d) {
                    System.out.println(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
                }
                System.out.println(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
            }
        }, "T3");
        T3.start();

        T3.join();
        System.out.println(ClassLayout.parseInstance(new Doge()).toPrintable());
    }
}

class Doge {
}

參考資料:

https://www.bilibili.com/video/BV1jE411j7uX?p=85

https://blog.csdn.net/qq_36434742/article/details/106854061

https://github.com/farmerjohngit/myblog/issues/12