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

鎖升級

  • 上文我們主要介紹什麼是偏向鎖,輕量級鎖,重量級鎖。並分析了三者的區別和使用場景。還記得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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章