在本文講解之前,先來簡單了解一下為什么會有批量重偏向和批量撤銷。
批量重偏向:當一個線程創建了大量對象並執行了初始的同步操作,后來另一個線程也來將這些對象作為鎖對象進行操作,會導偏向鎖重偏向的操作。
批量撤銷:在多線程競爭劇烈的情況下,使用偏向鎖將會降低效率,於是乎產生了批量撤銷機制。
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

重頭戲來了!線程t3也來競爭鎖。因為已經達到了批量撤銷的閾值,且對象listA.get(20)和listA.get(22)已經進行過偏向鎖的重偏向,並不會再次重偏向線程t3。
此時觸發批量撤銷,此時對象鎖膨脹變為輕量級鎖。
再來看看最后新生成的對象A。值得注意的是:本應該為可偏向狀態偏向鎖的新對象,在經歷過批量重偏向和批量撤銷后直接在實例化后轉為無鎖。
簡單總結
1、批量重偏向和批量撤銷是針對類的優化,和對象無關。
2、偏向鎖重偏向一次之后不可再次重偏向。
3、當某個類已經觸發批量撤銷機制后,JVM會默認當前類產生了嚴重的問題,剝奪了該類的新實例對象使用偏向鎖的權利