並發編程:synchronized 鎖升級過程的驗證


    關於synchronized關鍵字以及偏向鎖、輕量級鎖、重量級鎖的介紹廣大網友已經給出了太多文章和例子,這里就不再重復了,也可點擊鏈接來回顧一下。在這里來實戰操作一把,驗證JVM是怎么一步一步對鎖進行升級的,這其中有很多值得思考的地方。

需要關注的點:

  • JDK8偏向鎖默認是開啟的,不過JVM啟動后有4秒鍾的延遲,所以在這4秒鍾內對家加鎖都直接是輕量級鎖,可用-XX:BiasedLockingStartupDelay=0 關閉該特性

  • 測試用的JDK是64位的,所以獲取對象頭的時候是用unsafe.getLong,來獲取對象頭Markword的8個字節,如果你是32位則用unsafe.getInt替換即可

  • hashCode方法會對偏向鎖造成影響(這里的hashCode特指identity hashcode,如果鎖對象重載過hashCode方法則不會影響)

剩下的,我們直接代碼里來相見:

public class SynchronizedTest {
    public static void main(String[] args) throws Exception {
        // 直接休眠5秒,或者用-XX:BiasedLockingStartupDelay=0關閉偏向鎖延遲
        Thread.sleep(5000);
        // 反射獲取sun.misc的Unsafe對象,用來查看鎖的對象頭的信息
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        final Unsafe unsafe = (Unsafe) theUnsafe.get(null);

        // 鎖對象
        final Object lock = new Object();
        // TODO 64位JDK對象頭為 64bit = 8Byte,如果是32位JDK則需要換成unsafe.getInt
        printf("1_無鎖狀態:" + getLongBinaryString(unsafe.getLong(lock, 0L)));

        // 如果不執行hashCode方法,則對象頭的中的hashCode為0,
        // 但是如果執行了hashCode(identity hashcode,重載過的hashCode方法則不受影響),會導致偏向鎖的標識位變為0(不可偏向狀態),
        // 且后續的加鎖不會走偏向鎖而是直接到輕量級鎖(被hash的對象不可被用作偏向鎖)
//        lock.hashCode();
//        printf("鎖對象hash:" + getLongBinaryString(lock.hashCode()));

        printf("2_無鎖狀態:" + getLongBinaryString(unsafe.getLong(lock, 0L)));

        printf("主線程hash:" +getLongBinaryString(Thread.currentThread().hashCode()));
        printf("主線程ID:" +getLongBinaryString(Thread.currentThread().getId()) + "\n");
        // 無鎖 --> 偏向鎖
        new Thread(() -> {
            synchronized (lock) {
                printf("3_偏向鎖:" +getLongBinaryString(unsafe.getLong(lock, 0L)));
                printf("偏向線程hash:" +getLongBinaryString(Thread.currentThread().hashCode()));
                printf("偏向線程ID:" +getLongBinaryString(Thread.currentThread().getId()) + "\n");
                // 如果鎖對象已經進入了偏向狀態,再調用hashCode(),會導致鎖直接膨脹為重量級鎖
//                lock.hashCode();
            }
            // 再次進入同步快,lock鎖還是偏向當前線程
            synchronized (lock) {
                printf("4_偏向鎖:" +getLongBinaryString(unsafe.getLong(lock, 0L)));
                printf("偏向線程hash:" +getLongBinaryString(Thread.currentThread().hashCode()));
                printf("偏向線程ID:" +getLongBinaryString(Thread.currentThread().getId()) + "\n");
            }
        }).start();
        Thread.sleep(1000);

        // 可以看到就算偏向的線程結束,鎖對象的偏向鎖也不會自動撤銷
        printf("5_偏向線程結束:" +getLongBinaryString(unsafe.getLong(lock, 0L)) + "\n");

        // 偏向鎖 --> 輕量級鎖
        synchronized (lock) {
            // 對象頭為:指向線程棧中的鎖記錄指針
            printf("6_輕量級鎖:" + getLongBinaryString(unsafe.getLong(lock, 0L)));
            // 這里獲得輕量級鎖的線程是主線程
            printf("輕量級線程hash:" +getLongBinaryString(Thread.currentThread().hashCode()));
            printf("輕量級線程ID:" +getLongBinaryString(Thread.currentThread().getId()) + "\n");
        }
        new Thread(() -> {
            synchronized (lock) {
                printf("7_輕量級鎖:" +getLongBinaryString(unsafe.getLong(lock, 0L)));
                printf("輕量級線程hash:" +getLongBinaryString(Thread.currentThread().hashCode()));
                printf("輕量級線程ID:" +getLongBinaryString(Thread.currentThread().getId()) + "\n");
            }
        }).start();
        Thread.sleep(1000);

        // 輕量級鎖 --> 重量級鎖
        synchronized (lock) {
            int i = 123;
            // 注意:6_輕量級鎖 和 8_輕量級鎖 的對象頭是一樣的,證明線程釋放鎖后,棧幀中的鎖記錄並未清除,如果方法返回,鎖記錄是否保留還是清除?
            printf("8_輕量級鎖:" + getLongBinaryString(unsafe.getLong(lock, 0L)));
            // 在鎖已經獲取了lock的輕量級鎖的情況下,子線程來獲取鎖,則鎖會膨脹為重量級鎖
            new Thread(() -> {
                synchronized (lock) {
                    printf("9_重量級鎖:" +getLongBinaryString(unsafe.getLong(lock, 0L)));
                    printf("重量級線程hash:" +getLongBinaryString(Thread.currentThread().hashCode()));
                    printf("重量級線程ID:" +getLongBinaryString(Thread.currentThread().getId()) + "\n");
                }
            }).start();
            // 同步塊中睡眠1秒,不會釋放鎖,等待子線程請求鎖失敗導致鎖膨脹(見輕量級加鎖過程)
            Thread.sleep(1000);
        }
        Thread.sleep(500);
    }

    private static String getLongBinaryString(long num) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 64; i++) {
            if ((num & 1) == 1) {
                sb.append(1);
            } else {
                sb.append(0);
            }
            num = num >> 1;
        }
        return sb.reverse().toString();
    }
    private static void printf(String str) {
        System.out.printf("%s%n", str);
    }
}

運行結果如下:

1_無鎖狀態:0000000000000000000000000000000000000000000000000000000000000101
2_無鎖狀態:0000000000000000000000000000000000000000000000000000000000000101
主線程hash:0000000000000000000000000000000001001010010101110100011110010101
主線程ID:0000000000000000000000000000000000000000000000000000000000000001

3_偏向鎖:0000000000000000000000000000000000011110001001011110100000000101
偏向線程hash:0000000000000000000000000000000001001011010110110100011011111101
偏向線程ID:0000000000000000000000000000000000000000000000000000000000001010

4_偏向鎖:0000000000000000000000000000000000011110001001011110100000000101
偏向線程hash:0000000000000000000000000000000001001011010110110100011011111101
偏向線程ID:0000000000000000000000000000000000000000000000000000000000001010

5_偏向線程結束:0000000000000000000000000000000000011110001001011110100000000101

6_輕量級鎖:0000000000000000000000000000000000000011000110101111010010110000
輕量級線程hash:0000000000000000000000000000000001001010010101110100011110010101
輕量級線程ID:0000000000000000000000000000000000000000000000000000000000000001

7_輕量級鎖:0000000000000000000000000000000000011110101101101111010010001000
輕量級線程hash:0000000000000000000000000000000000011000010110111010100010100100
輕量級線程ID:0000000000000000000000000000000000000000000000000000000000001011

8_輕量級鎖:0000000000000000000000000000000000000011000110101111010010110000
9_重量級鎖:0000000000000000000000000000000000000011010010101110000100011010
重量級線程hash:0000000000000000000000000000000000111101101111111101111111000111
重量級線程ID:0000000000000000000000000000000000000000000000000000000000001100

現在依此來看下各個狀態:

  • 1_無鎖狀態:通過結果可以看到:對象的hashCode為0,gc分代年齡也是0,偏向鎖標志位為1(表示可偏向狀態),鎖標志位為01

  • 2_無鎖狀態:如果不執行hashCode方法,則跟1_無鎖狀態一致,否則為:0000000000000000000000000100101001010111010001111001010100000001
    偏向鎖標志位為0,表示不可偏向狀態,這里網友們大多有誤解,實際應該為:偏向鎖標志位表示的是當前鎖是否可偏向

  • 3_偏向鎖:子線程首次獲取鎖,則鎖偏向子線程

  • 4_偏向鎖:子線程是否鎖后再次獲取鎖,JVM檢測到鎖是偏向子線程的,所以直接獲取

  • 5_偏向線程結束:偏向的線程結束后,鎖對象的對象頭沒有改變,所以偏向鎖也不會自動撤銷(這里JDK團隊是否可以做優化呢?還是說線程根本就沒記錄哪些鎖偏向了自己,所以退出的時候也沒法一一撤銷)

  • 6_輕量級鎖:如果鎖已經偏向了一個線程,則其他現在來獲取鎖,則需要升級為輕量級鎖

  • 7_輕量級鎖:只要沒有多個線程同一時刻來競爭鎖,則多個線程可以輪流使用這把輕量級鎖(使用完后會及時釋放,CAS替換Markword)

  • 8_輕量級鎖、9_重量級鎖:主線程先獲取輕量級鎖,在持有鎖的同時,創建一個子線程來獲取同一把鎖,這時候有了鎖的競爭,則會升級為重量級鎖

注意:

如果把代碼里的第一行或者第二行lock.hashCode();注釋掉的話,則執行的結果完全就不同了,也可從結果驗證上文提到的hashCode對偏向鎖的影響。

還剩一個問題:

網上經常能看到的一張對象頭布局圖,其中偏向鎖狀態時Markword存儲的是:線程ID + Epoch + 分代年齡 + 1 + 01


但是,我在程序中驗證了,鎖對象處於偏向鎖的狀態時,Markword存儲的內容既不是線程ID也不是線程對象的hashCode,這個問題很奇怪,目前還沒找到原因所在。


免責聲明!

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



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