偏向鎖理論太抽象,實戰了解下偏向鎖如何發生以及如何升級【實戰篇】

煙花散盡13141發表於2022-04-18

鎖升級

  • 上文我們主要介紹什麼是偏向鎖,輕量級鎖,重量級鎖。並分析了三者的區別和使用場景。還記得Redis章節中整數集中升級操作嗎。在鎖中我們同樣是設計鎖升級和降級的。上文我們也介紹了當沒有競爭時偏向鎖,出現競爭時就輕量級鎖。
  • 但是輕量級鎖時cas操作和自旋等待。自旋只能適合併發少的情況,如果併發很多一個執行緒可能需要等待很久才能獲取到鎖,那麼自旋期間的開銷也是很巨大的,所以就很有必要升級輕量級鎖。那麼什麼時候該升級重量級鎖呢?JVM中也是設定了自旋次數的,超過一定次數就會發生升級成重量級鎖

偏向鎖升級輕量級鎖

  • 個人認為重點還是偏向鎖升級的過程。因為偏向鎖不會主動撤銷,所以鎖升級過程涉及批量鎖撤銷,批量鎖偏向等場景。

image-20211213152554303.png

  • 還記得偏向鎖在鎖物件的markword中的儲存結構嗎,末尾三位是101表示偏向鎖。關於Lock Record就是上面我們提到的執行緒棧頂的鎖記錄物件的指標,關於鎖記錄內部儲存了整個鎖物件的markword , 而這裡我們需要注意的是EPOCH , EPOCH翻譯過來是紀元的意思。我們簡單理解成版本好
  • 說到版本號,我們還得熟悉JVM關於偏向鎖的兩個屬性設定

image-20211213153045152.png

  • 發生輕量級鎖升級的時候就會發生偏向鎖的撤銷。如果JVM發現某一類鎖發生鎖撤銷的次數大於等於-XX:BiasedLockIngBulkRebiasThreshold=20時,就會宣佈偏向鎖失效。讓偏向鎖失效就是將版本號加1 即 EPOCH+1;
  • 當一個類鎖發生的總撤銷數大於等於-XX:BiasedLockingBulkRevokeThreshold=40,則後續在上鎖會預設上輕量級鎖。
class Demo{
    String userName;
}
public class LockRevoke {
    public static void main(String[] args) throws InterruptedException {
        List<Demo> list = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            list.add(new Demo());
        }
        final Thread t1 = new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                for (int i = 0; i < 99; i++) {
                    Demo demo = list.get(i);
                    synchronized (demo) {
                    }
                }
                TimeUnit.SECONDS.sleep(100000);
            }
        });

        final Thread t2 = new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                synchronized (list.get(99)) {
                    System.out.println("第100個物件上鎖中,並持續使用該物件" + ClassLayout.parseInstance(list.get(99)).toPrintable());
                    TimeUnit.SECONDS.sleep(99999);
                }
            }
        });

        final Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 40; i++) {
                    Demo demo = list.get(i);
                    synchronized (demo) {


                        if (i == 18) {
                            System.out.println("傳送第19次鎖升級,list.get(18)應該是輕量級鎖" + ClassLayout.parseInstance(list.get(18)).toPrintable());
                        }
                        if (i == 19) {
                            System.out.println("傳送第20次鎖升級,會發生批量重偏向;紀元+1;後續偏向鎖都會偏向當前執行緒;list.get(19)應該是輕量級鎖" + ClassLayout.parseInstance(list.get(19)).toPrintable());
                            System.out.println("因為第100物件仍然在使用,需要修改起紀元" + ClassLayout.parseInstance(list.get(99)).toPrintable());
                        }
                        if (i == 29) {
                            System.out.println("在批量重偏向之後;因為第一次偏向鎖已經失效了,所以這裡不是輕量級而是偏向該執行緒的偏向鎖" + ClassLayout.parseInstance(list.get(29)).toPrintable());
                        }
                        if (i == 39) {
                            System.out.println("傳送第40次鎖升級,發生批量鎖撤銷;這裡應該是輕量級鎖後續都是輕量級" + ClassLayout.parseInstance(list.get(39)).toPrintable());
                        }
                    }
                }

            }
        });
        
        t1.start();
        t2.start();
        TimeUnit.SECONDS.sleep(5);
        System.out.println("第一次上鎖後list.get(0)應該偏向鎖:" + ClassLayout.parseInstance(list.get(0)).toPrintable());
        System.out.println("第一次上鎖後list.get(19)應該偏向鎖:" + ClassLayout.parseInstance(list.get(19)).toPrintable());
        System.out.println("第一次上鎖後list.get(29)應該偏向鎖:" + ClassLayout.parseInstance(list.get(29)).toPrintable());
        System.out.println("第一次上鎖後list.get(39)應該偏向鎖:" + ClassLayout.parseInstance(list.get(39)).toPrintable());
        System.out.println("第一次上鎖後list.get(99)應該偏向鎖:" + ClassLayout.parseInstance(list.get(99)).toPrintable());
        t3.start();
       

    }

}
  • 上面就是典型的偏向鎖重偏向和偏向鎖撤銷案列整合。
  • 首先我們t1執行緒率先將前99個物件都上鎖並立馬釋放,因為我們的vm設定取消偏向鎖延遲了,如何設定請看文章開頭部分。
  • 第2個執行緒t2只對最後一個物件進行上鎖,不同的是上鎖後永久佔著不釋放。那麼別人就無法獲取到最後一個物件的鎖
  • 第3個執行緒開始和上面初始化好的物件進行搶佔資源。第三個執行緒只迴圈了40次,因為JVM預設的最大撤銷偏向鎖次數就是40次。後面都是輕量級鎖了。
  • 因為第3個執行緒會發生批量重偏向,所以後續不會造成偏向鎖撤銷。如果像看到批量鎖撤銷,就必須在開一個執行緒上鎖。所以執行緒4就是繼續造成撤銷,但是要保證執行緒4後執行,否則t3,t4同時執行會造成重量級鎖,因為重量級鎖的場景之一就是:1個偏向鎖,1個輕量級鎖,1個正在請求就會出發重量級鎖
  • 在第三個執行緒中對i==18即第19個元素進行上鎖時,因為之前已經被上了偏向鎖,雖然被釋放了鎖,但是偏向鎖本身並不會釋放,這個前面也已經鋪墊了。所以此時第19個元素先發生鎖撤銷,然後在上輕量級鎖。所以這裡預測第19個物件時輕量級鎖
  • 然後來到i19,即第20個元素,因為JVM預設類總撤銷大於等於20會發生批量重偏向。啥意思呢?在t3 中i19之前上鎖都是輕量級。i19之後在上鎖就會時偏向鎖,只不過是偏向執行緒3的,而不是偏向執行緒1的。這裡我們可以和第一次的i19記憶體佈局進行對比,除了執行緒id不一樣還有一個紀元不一樣,

image-20211213161634774.png

  • 上面為什麼我會單獨起一個執行緒鎖定list.get(99)呢?就是為了測試當發生批量重偏向的時候能夠直觀看到正在使用的鎖紀元資訊被修改,以免造成鎖丟棄

image-20211213162536276.png

  • 我們能夠看的出來在發生批量重偏向的時候,正在使用的鎖紀元資訊會被更新,如果不更新會被JVM認為是廢棄偏向鎖。當然發生批量重偏向後再次獲取物件鎖就不會在發生鎖撤銷了。因為之前的鎖已經廢棄了,所以我們獲取一下後續的鎖資訊,這裡就看看list.get(29)吧。

image-20211213162904537.png

  • 第4個執行緒在第三個執行緒之後不斷造成撤銷,將達到撤銷總數40的時候,JVM就會認為後續該類的鎖不適合做偏向鎖了,直接就是輕量級鎖

image-20211213164055601.png

相關文章