在本文講解之前,先來簡單瞭解一下為什麼會有批量重偏向和批量撤銷。
批量重偏向:當一個執行緒建立了大量物件並執行了初始的同步操作,後來另一個執行緒也來將這些物件作為鎖物件進行操作,會導偏向鎖重偏向的操作。
批量撤銷:在多執行緒競爭劇烈的情況下,使用偏向鎖將會降低效率,於是乎產生了批量撤銷機制。
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會預設當前類產生了嚴重的問題,剝奪了該類的新例項物件使用偏向