關於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,這個問題很奇怪,目前還沒找到原因所在。
