App下載

再談synchronized鎖升級(jí)

猿友 2021-05-14 12:00:13 瀏覽數(shù) (2968)
反饋

圖文詳解Java對(duì)象內(nèi)存布局這篇文章中,在研究對(duì)象頭時(shí)我們了解了synchronized鎖升級(jí)的過(guò)程,由于篇幅有限,對(duì)鎖升級(jí)的過(guò)程介紹的比較簡(jiǎn)略,本文在上一篇的基礎(chǔ)上,來(lái)詳細(xì)研究一下鎖升級(jí)的過(guò)程以及各個(gè)狀態(tài)下鎖的原理。本文結(jié)構(gòu)如下:

  • 1 無(wú)鎖
  • 2 偏向鎖
  • 3 輕量級(jí)鎖
  • 4 重量級(jí)鎖
  • 總結(jié)

1 無(wú)鎖

在上一篇文章中,我們提到過(guò) jvm 會(huì)有4秒的偏向鎖開(kāi)啟的延遲時(shí)間,在這個(gè)偏向延遲內(nèi)對(duì)象處于為無(wú)鎖態(tài)。如果關(guān)閉偏向鎖啟動(dòng)延遲、或是經(jīng)過(guò)4秒且沒(méi)有線程競(jìng)爭(zhēng)對(duì)象的鎖,那么對(duì)象會(huì)進(jìn)入無(wú)鎖可偏向狀態(tài)。

準(zhǔn)確來(lái)說(shuō),無(wú)鎖可偏向狀態(tài)應(yīng)該叫做匿名偏向(Anonymously biased)狀態(tài),因?yàn)檫@時(shí)對(duì)象的mark word中后三位已經(jīng)是101,但是threadId指針部分仍然全部為 0,它還沒(méi)有向任何線程偏向。綜上所述,對(duì)象在剛被創(chuàng)建時(shí),根據(jù) jvm 的配置對(duì)象可能會(huì)處于 無(wú)鎖匿名偏向 兩個(gè)狀態(tài)。

此外,如果在 jvm 的參數(shù)中關(guān)閉偏向鎖,那么直到有線程獲取這個(gè)鎖對(duì)象之前,會(huì)一直處于無(wú)鎖不可偏向狀態(tài)。修改 jvm 啟動(dòng)參數(shù):

-XX:-UseBiasedLocking

延遲5s后打印對(duì)象內(nèi)存布局:

public static void main(String[] args) throws InterruptedException {
    User user=new User();
    TimeUnit.SECONDS.sleep(5);
    System.out.println(ClassLayout.parseInstance(user).toPrintable());
}

延遲5s后打印對(duì)象內(nèi)存布局

可以看到,即使經(jīng)過(guò)一定的啟動(dòng)延時(shí),對(duì)象一直處于001無(wú)鎖不可偏向狀態(tài)。大家可能會(huì)有疑問(wèn),在無(wú)鎖狀態(tài)下,為什么要存在一個(gè)不可偏向狀態(tài)呢?通過(guò)查閱資料得到的解釋是:

JVM內(nèi)部的代碼有很多地方也用到了synchronized,明確在這些地方存在線程的競(jìng)爭(zhēng),如果還需要從偏向狀態(tài)再逐步升級(jí),會(huì)帶來(lái)額外的性能損耗,所以JVM設(shè)置了一個(gè)偏向鎖的啟動(dòng)延遲,來(lái)降低性能損耗

也就是說(shuō),在無(wú)鎖不可偏向狀態(tài)下,如果有線程試圖獲取鎖,那么將跳過(guò)升級(jí)偏向鎖的過(guò)程,直接使用輕量級(jí)鎖。使用代碼進(jìn)行驗(yàn)證:

//-XX:-UseBiasedLocking
public static void main(String[] args) throws InterruptedException {
    User user=new User();
    synchronized (user){
        System.out.println(ClassLayout.parseInstance(user).toPrintable());
    }
}

查看結(jié)果可以看到,在關(guān)閉偏向鎖情況下使用synchronized,鎖會(huì)直接升級(jí)為輕量級(jí)鎖(00狀態(tài)):

在關(guān)閉偏向鎖情況下使用synchronized

在目前的基礎(chǔ)上,可以用流程圖概括上面的過(guò)程:

在關(guān)閉偏向鎖情況下使用synchronized流程圖

額外注意一點(diǎn)就是匿名偏向狀態(tài)下,如果調(diào)用系統(tǒng)的hashCode()方法,會(huì)使對(duì)象回到無(wú)鎖態(tài),并在markword中寫入hashCode。并且在這個(gè)狀態(tài)下,如果有線程嘗試獲取鎖,會(huì)直接從無(wú)鎖升級(jí)到輕量級(jí)鎖,不會(huì)再升級(jí)為偏向鎖。

2 偏向鎖

2.1 偏向鎖原理

匿名偏向狀態(tài)是偏向鎖的初始狀態(tài),在這個(gè)狀態(tài)下第一個(gè)試圖獲取該對(duì)象的鎖的線程,會(huì)使用CAS操作(匯編命令CMPXCHG)嘗試將自己的threadID寫入對(duì)象頭的mark word中,使匿名偏向狀態(tài)升級(jí)為已偏向(Biased)的偏向鎖狀態(tài)。在已偏向狀態(tài)下,線程指針threadID非空,且偏向鎖的時(shí)間戳epoch為有效值。

如果之后有線程再次嘗試獲取鎖時(shí),需要檢查mark word中存儲(chǔ)的threadID是否與自己相同即可,如果相同那么表示當(dāng)前線程已經(jīng)獲得了對(duì)象的鎖,不需要再使用CAS操作來(lái)進(jìn)行加鎖。

如果mark word中存儲(chǔ)的threadID與當(dāng)前線程不同,那么將執(zhí)行CAS操作,試圖將當(dāng)前線程的ID替換mark word中的threadID。只有當(dāng)對(duì)象處于下面兩種狀態(tài)中時(shí),才可以執(zhí)行成功:

  • 對(duì)象處于匿名偏向狀態(tài)
  • 對(duì)象處于可重偏向(Rebiasable)狀態(tài),新線程可使用CAS將threadID指向自己

如果對(duì)象不處于上面兩個(gè)狀態(tài),說(shuō)明鎖存在線程競(jìng)爭(zhēng),在CAS替換失敗后會(huì)執(zhí)行偏向鎖撤銷操作。偏向鎖的撤銷需要等待全局安全點(diǎn)Safe Point(安全點(diǎn)是 jvm為了保證在垃圾回收的過(guò)程中引用關(guān)系不會(huì)發(fā)生變化設(shè)置的安全狀態(tài),在這個(gè)狀態(tài)上會(huì)暫停所有線程工作),在這個(gè)安全點(diǎn)會(huì)掛起獲得偏向鎖的線程。

在暫停線程后,會(huì)通過(guò)遍歷當(dāng)前jvm的所有線程的方式,檢查持有偏向鎖的線程狀態(tài)是否存活:

  • 如果線程還存活,且線程正在執(zhí)行同步代碼塊中的代碼,則升級(jí)為輕量級(jí)鎖

  • 如果持有偏向鎖的線程未存活,或者持有偏向鎖的線程未在執(zhí)行同步代碼塊中的代碼,則進(jìn)行校驗(yàn)是否允許重偏向:

    • 不允許重偏向,則撤銷偏向鎖,將mark word升級(jí)為輕量級(jí)鎖,進(jìn)行 CAS 競(jìng)爭(zhēng)鎖
    • 允許重偏向,設(shè)置為匿名偏向鎖狀態(tài),CAS 將偏向鎖重新指向新線程

完成上面的操作后,喚醒暫停的線程,從安全點(diǎn)繼續(xù)執(zhí)行代碼??梢允褂昧鞒虉D總結(jié)上面的過(guò)程:

偏向鎖原理流程圖

2.2 偏向鎖升級(jí)

在上面的過(guò)程中,我們已經(jīng)知道了匿名偏向狀態(tài)可以變?yōu)闊o(wú)鎖態(tài)或升級(jí)為偏向鎖,接下來(lái)看一下偏向鎖的其他狀態(tài)的改變

  • 偏向鎖升級(jí)為輕量級(jí)鎖

public static void main(String[] args) throws InterruptedException {
    User user=new User();
    synchronized (user){
        System.out.println(ClassLayout.parseInstance(user).toPrintable());
    }
    Thread thread = new Thread(() -> {
        synchronized (user) {
            System.out.println("--THREAD--:"+ClassLayout.parseInstance(user).toPrintable());
        }
    });
    thread.start();
    thread.join();
    System.out.println("--END--:"+ClassLayout.parseInstance(user).toPrintable());
}

查看內(nèi)存布局,偏向鎖升級(jí)為輕量級(jí)鎖,在執(zhí)行完成同步代碼后釋放鎖,變?yōu)闊o(wú)鎖不可偏向狀態(tài):

偏向鎖升級(jí)

  • 偏向鎖升級(jí)為重量級(jí)鎖

public static void main(String[] args) throws InterruptedException {
    User user=new User();
    Thread thread = new Thread(() -> {
        synchronized (user) {
            System.out.println("--THREAD1--:" + ClassLayout.parseInstance(user).toPrintable());
            try {
                user.wait(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("--THREAD END--:" + ClassLayout.parseInstance(user).toPrintable());
        }
    });
    thread.start();
    thread.join();
    TimeUnit.SECONDS.sleep(3);
    System.out.println(ClassLayout.parseInstance(user).toPrintable());
}

查看內(nèi)存布局,可以看到在調(diào)用了對(duì)象的wait()方法后,直接從偏向鎖升級(jí)成了重量級(jí)鎖,并在鎖釋放后變?yōu)闊o(wú)鎖態(tài):

偏向鎖升級(jí)為重量級(jí)鎖

這里是因?yàn)?code>wait()方法調(diào)用過(guò)程中依賴于重量級(jí)鎖中與對(duì)象關(guān)聯(lián)的monitor,在調(diào)用wait()方法后monitor會(huì)把線程變?yōu)?code>WAITING狀態(tài),所以才會(huì)強(qiáng)制升級(jí)為重量級(jí)鎖。除此之外,調(diào)用hashCode方法時(shí)也會(huì)使偏向鎖直接升級(jí)為重量級(jí)鎖。

在上面分析的基礎(chǔ)上,再加上我們上一篇中講到的輕量級(jí)鎖升級(jí)到重量級(jí)鎖的知識(shí),就可以對(duì)上面的流程圖進(jìn)行完善了:

偏向鎖升級(jí)為重量級(jí)鎖流程圖

2.3 批量重偏向

在未禁用偏向鎖的情況下,當(dāng)一個(gè)線程建立了大量對(duì)象,并且對(duì)它們執(zhí)行完同步操作解鎖后,所有對(duì)象處于偏向鎖狀態(tài),此時(shí)若再來(lái)另一個(gè)線程也嘗試獲取這些對(duì)象的鎖,就會(huì)導(dǎo)偏向鎖的批量重偏向(Bulk Rebias)。當(dāng)觸發(fā)批量重偏向后,第一個(gè)線程結(jié)束同步操作后的鎖對(duì)象當(dāng)再被同步訪問(wèn)時(shí)會(huì)被重置為可重偏向狀態(tài),以便允許快速重偏向,這樣能夠減少撤銷偏向鎖再升級(jí)為輕量級(jí)鎖的性能消耗。

首先看一下和偏向鎖有關(guān)的參數(shù),修改jvm啟動(dòng)參數(shù),使用下面的命令可以在項(xiàng)目啟動(dòng)時(shí)打印jvm的默認(rèn)參數(shù)值:

-XX:+PrintFlagsFinal

需要關(guān)注的屬性有下面3個(gè):

批量重偏向

  • BiasedLockingBulkRebiasThreshold:偏向鎖批量重偏向閾值,默認(rèn)為20次
  • BiasedLockingBulkRevokeThreshold:偏向鎖批量撤銷閾值,默認(rèn)為40次
  • BiasedLockingDecayTime:重置計(jì)數(shù)的延遲時(shí)間,默認(rèn)值為25000毫秒(即25秒)

批量重偏向是以class而不是對(duì)象為單位的,每個(gè)class會(huì)維護(hù)一個(gè)偏向鎖的撤銷計(jì)數(shù)器,每當(dāng)該class的對(duì)象發(fā)生偏向鎖的撤銷時(shí),該計(jì)數(shù)器會(huì)加一,當(dāng)這個(gè)值達(dá)到默認(rèn)閾值20時(shí),jvm就會(huì)認(rèn)為這個(gè)鎖對(duì)象不再適合原線程,因此進(jìn)行批量重偏向。而距離上次批量重偏向的25秒內(nèi),如果撤銷計(jì)數(shù)達(dá)到40,就會(huì)發(fā)生批量撤銷,如果超過(guò)25秒,那么就會(huì)重置在[20, 40)內(nèi)的計(jì)數(shù)。

上面這段理論是不是聽(tīng)上去有些難理解,沒(méi)關(guān)系,我們先用代碼驗(yàn)證批量重偏向的過(guò)程:

private static Thread t1,t2;
public static void main(String[] args) throws InterruptedException {      
    TimeUnit.SECONDS.sleep(5);
    List<Object> list = new ArrayList<>();
    for (int i = 0; i < 40; i++) {
        list.add(new Object());
    }


    t1 = new Thread(() -> {
        for (int i = 0; i < list.size(); i++) {
            synchronized (list.get(i)) {
            }
        }
        LockSupport.unpark(t2);
    });
    t2 = new Thread(() -> {
        LockSupport.park();
        for (int i = 0; i < 30; i++) {
            Object o = list.get(i);
            synchronized (o) {
                if (i == 18 || i == 19) {
                    System.out.println("THREAD-2 Object"+(i+1)+":"+ClassLayout.parseInstance(o).toPrintable());
                }
            }
        }
    });
    t1.start();
    t2.start();
    t2.join();


    TimeUnit.SECONDS.sleep(3);
    System.out.println("Object19:"+ClassLayout.parseInstance(list.get(18)).toPrintable());
    System.out.println("Object20:"+ClassLayout.parseInstance(list.get(19)).toPrintable());
    System.out.println("Object30:"+ClassLayout.parseInstance(list.get(29)).toPrintable());
    System.out.println("Object31:"+ClassLayout.parseInstance(list.get(30)).toPrintable());
}

分析上面的代碼,當(dāng)線程t1運(yùn)行結(jié)束后,數(shù)組中所有對(duì)象的鎖都偏向t1,然后t1喚醒被掛起的線程t2,線程t2嘗試獲取前30個(gè)對(duì)象的鎖。我們打印線程t2獲取到的第19和第20個(gè)對(duì)象的鎖狀態(tài):

批量重偏向

線程t2在訪問(wèn)前19個(gè)對(duì)象時(shí)對(duì)象的偏向鎖會(huì)升級(jí)到輕量級(jí)鎖,在訪問(wèn)后11個(gè)對(duì)象(下標(biāo)19-29)時(shí),因?yàn)槠蜴i撤銷次數(shù)達(dá)到了20,會(huì)觸發(fā)批量重偏向,將鎖的狀態(tài)變?yōu)槠蚓€程t2。在全部線程結(jié)束后,再次查看第19、20、30、31個(gè)對(duì)象鎖的狀態(tài):

批量重偏向

線程t2結(jié)束后,第1-19的對(duì)象釋放輕量級(jí)鎖變?yōu)闊o(wú)鎖不可偏向狀態(tài),第20-30的對(duì)象狀態(tài)為偏向鎖、但從偏向t1改為偏向t2,第31-40的對(duì)象因?yàn)闆](méi)有被線程t2訪問(wèn)所以保持偏向線程t1不變。

2.4 批量撤銷

在多線程競(jìng)爭(zhēng)激烈的狀況下,使用偏向鎖將會(huì)導(dǎo)致性能降低,因此產(chǎn)生了批量撤銷機(jī)制,接下來(lái)使用代碼進(jìn)行測(cè)試:

private static Thread t1, t2, t3;
public static void main(String[] args) throws InterruptedException {
    TimeUnit.SECONDS.sleep(5);


    List<Object> list = new ArrayList<>();
    for (int i = 0; i < 40; i++) {
        list.add(new Object());
    }


    t1 = new Thread(() -> {
        for (int i = 0; i < list.size(); i++) {
            synchronized (list.get(i)) {
            }
        }
        LockSupport.unpark(t2);
    });
    t2 = new Thread(() -> {
        LockSupport.park();
        for (int i = 0; i < list.size(); i++) {
            Object o = list.get(i);
            synchronized (o) {
                if (i == 18 || i == 19) {
                    System.out.println("THREAD-2 Object"+(i+1)+":"+ClassLayout.parseInstance(o).toPrintable());
                }
            }
        }
        LockSupport.unpark(t3);
    });
    t3 = new Thread(() -> {
        LockSupport.park();
        for (int i = 0; i < list.size(); i++) {
            Object o = list.get(i);
            synchronized (o) {
                System.out.println("THREAD-3 Object"+(i+1)+":"+ClassLayout.parseInstance(o).toPrintable());
            }
        }
    });


    t1.start();
    t2.start();
    t3.start();
    t3.join();
    System.out.println("New: "+ClassLayout.parseInstance(new Object()).toPrintable());
}

對(duì)上面的運(yùn)行流程進(jìn)行分析:

  • 線程t1中,第1-40的鎖對(duì)象狀態(tài)變?yōu)槠蜴i
  • 線程t2中,第1-19的鎖對(duì)象撤銷偏向鎖升級(jí)為輕量級(jí)鎖,然后對(duì)第20-40的對(duì)象進(jìn)行批量重偏向
  • 線程t3中,首先直接對(duì)第1-19個(gè)對(duì)象競(jìng)爭(zhēng)輕量級(jí)鎖,而從第20個(gè)對(duì)象開(kāi)始往后的對(duì)象不會(huì)再次進(jìn)行批量重偏向,因此第20-39的對(duì)象進(jìn)行偏向鎖撤銷升級(jí)為輕量級(jí)鎖,這時(shí)t2t3線程一共執(zhí)行了40次的鎖撤銷,觸發(fā)鎖的批量撤銷機(jī)制,對(duì)偏向鎖進(jìn)行撤銷置為輕量級(jí)鎖

看一下在3個(gè)線程都結(jié)束后創(chuàng)建的新對(duì)象:

批量撤銷

可以看到,創(chuàng)建的新對(duì)象為無(wú)鎖不可偏向狀態(tài)001,說(shuō)明當(dāng)類觸發(fā)了批量撤銷機(jī)制后,jvm 會(huì)禁用該類創(chuàng)建對(duì)象時(shí)的可偏向性,該類新創(chuàng)建的對(duì)象全部為無(wú)鎖不可偏向狀態(tài)。

2.5 總結(jié)

偏向鎖通過(guò)消除資源無(wú)競(jìng)爭(zhēng)情況下的同步原語(yǔ),提高了程序在單線程下訪問(wèn)同步資源的運(yùn)行性能,但是當(dāng)出現(xiàn)多個(gè)線程競(jìng)爭(zhēng)時(shí),就會(huì)撤銷偏向鎖、升級(jí)為輕量級(jí)鎖。

如果我們的應(yīng)用系統(tǒng)是高并發(fā)、并且代碼中同步資源一直是被多線程訪問(wèn)的,那么撤銷偏向鎖這一步就顯得多余,偏向鎖撤銷時(shí)進(jìn)入Safe Point產(chǎn)生STW的現(xiàn)象應(yīng)該是被極力避免的,這時(shí)應(yīng)該通過(guò)禁用偏向鎖來(lái)減少性能上的損耗。

3 輕量級(jí)鎖

3.1 輕量級(jí)鎖原理

1、在代碼訪問(wèn)同步資源時(shí),如果鎖對(duì)象處于無(wú)鎖不可偏向狀態(tài),jvm首先將在當(dāng)前線程的棧幀中創(chuàng)建一條鎖記錄(lock record),用于存放:

  • displaced mark word(置換標(biāo)記字):存放鎖對(duì)象當(dāng)前的mark word的拷貝
  • owner指針:指向當(dāng)前的鎖對(duì)象的指針,在拷貝mark word階段暫時(shí)不會(huì)處理它

 輕量級(jí)鎖原理

2、在拷貝mark word完成后,首先會(huì)掛起線程,jvm使用CAS操作嘗試將對(duì)象的 mark word 中的 lock record 指針指向棧幀中的鎖記錄,并將鎖記錄中的owner指針指向鎖對(duì)象的mark word

  • 如果CAS替換成功,表示競(jìng)爭(zhēng)鎖對(duì)象成功,則將鎖標(biāo)志位設(shè)置成 00,表示對(duì)象處于輕量級(jí)鎖狀態(tài),執(zhí)行同步代碼中的操作

輕量級(jí)鎖原理

  • 如果CAS替換失敗,則判斷當(dāng)前對(duì)象的mark word是否指向當(dāng)前線程的棧幀:

    • 如果是則表示當(dāng)前線程已經(jīng)持有對(duì)象的鎖,執(zhí)行的是synchronized的鎖重入過(guò)程,可以直接執(zhí)行同步代碼塊
    • 否則說(shuō)明該其他線程已經(jīng)持有了該對(duì)象的鎖,如果在自旋一定次數(shù)后仍未獲得鎖,那么輕量級(jí)鎖需要升級(jí)為重量級(jí)鎖,將鎖標(biāo)志位變成10,后面等待的線程將會(huì)進(jìn)入阻塞狀態(tài)

4、輕量級(jí)鎖的釋放同樣使用了CAS操作,嘗試將displaced mark word替換回mark word,這時(shí)需要檢查鎖對(duì)象的mark wordlock record指針是否指向當(dāng)前線程的鎖記錄:

  • 如果替換成功,則表示沒(méi)有競(jìng)爭(zhēng)發(fā)生,整個(gè)同步過(guò)程就完成了
  • 如果替換失敗,則表示當(dāng)前鎖資源存在競(jìng)爭(zhēng),有可能其他線程在這段時(shí)間里嘗試過(guò)獲取鎖失敗,導(dǎo)致自身被掛起,并修改了鎖對(duì)象的mark word升級(jí)為重量級(jí)鎖,最后在執(zhí)行重量級(jí)鎖的解鎖流程后喚醒被掛起的線程

用流程圖對(duì)上面的過(guò)程進(jìn)行描述:

輕量級(jí)鎖原理流程圖

3.2 輕量級(jí)鎖重入

我們知道,synchronized是可以鎖重入的,在輕量級(jí)鎖的情況下重入也是依賴于棧上的lock record完成的。以下面的代碼中3次鎖重入為例:

synchronized (user){
    synchronized (user){
        synchronized (user){
            //TODO
        }
    }
}

輕量級(jí)鎖的每次重入,都會(huì)在棧中生成一個(gè)lock record,但是保存的數(shù)據(jù)不同:

  • 首次分配的lock record,displaced mark word復(fù)制了鎖對(duì)象的mark word,owner指針指向鎖對(duì)象
  • 之后重入時(shí)在棧中分配的lock record中的displaced mark wordnull,只存儲(chǔ)了指向?qū)ο蟮?code>owner指針

輕量級(jí)鎖重入

輕量級(jí)鎖中,重入的次數(shù)等于該鎖對(duì)象在棧幀中lock record的數(shù)量,這個(gè)數(shù)量隱式地充當(dāng)了鎖重入機(jī)制的計(jì)數(shù)器。這里需要計(jì)數(shù)的原因是每次解鎖都需要對(duì)應(yīng)一次加鎖,只有最后解鎖次數(shù)等于加鎖次數(shù)時(shí),鎖對(duì)象才會(huì)被真正釋放。在釋放鎖的過(guò)程中,如果是重入則刪除棧中的lock record,直到?jīng)]有重入時(shí)則使用CAS替換鎖對(duì)象的mark word

3.3 輕量級(jí)鎖升級(jí)

在jdk1.6以前,默認(rèn)輕量級(jí)鎖自旋次數(shù)是10次,如果超過(guò)這個(gè)次數(shù)或自旋線程數(shù)超過(guò)CPU核數(shù)的一半,就會(huì)升級(jí)為重量級(jí)鎖。這時(shí)因?yàn)槿绻孕螖?shù)過(guò)多,或過(guò)多線程進(jìn)入自旋,會(huì)導(dǎo)致消耗過(guò)多cpu資源,重量級(jí)鎖情況下線程進(jìn)入等待隊(duì)列可以降低cpu資源的消耗。自旋次數(shù)的值也可以通過(guò)jvm參數(shù)進(jìn)行修改:

-XX:PreBlockSpin

jdk1.6以后加入了自適應(yīng)自旋鎖Adapative Self Spinning),自旋的次數(shù)不再固定,由jvm自己控制,由前一次在同一個(gè)鎖上的自旋時(shí)間及鎖的擁有者的狀態(tài)來(lái)決定:

  • 對(duì)于某個(gè)鎖對(duì)象,如果自旋等待剛剛成功獲得過(guò)鎖,并且持有鎖的線程正在運(yùn)行中,那么虛擬機(jī)就會(huì)認(rèn)為這次自旋也是很有可能再次成功,進(jìn)而允許自旋等待持續(xù)相對(duì)更長(zhǎng)時(shí)間
  • 對(duì)于某個(gè)鎖對(duì)象,如果自旋很少成功獲得過(guò)鎖,那在以后嘗試獲取這個(gè)鎖時(shí)將可能省略掉自旋過(guò)程,直接阻塞線程,避免浪費(fèi)處理器資源。

下面通過(guò)代碼驗(yàn)證輕量級(jí)鎖升級(jí)為重量級(jí)鎖的過(guò)程:

public static void main(String[] args) throws InterruptedException {
    User user = new User();
    System.out.println("--MAIN--:" + ClassLayout.parseInstance(user).toPrintable());
    Thread thread1 = new Thread(() -> {
        synchronized (user) {
            System.out.println("--THREAD1--:" + ClassLayout.parseInstance(user).toPrintable());
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    Thread thread2 = new Thread(() -> {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (user) {
            System.out.println("--THREAD2--:" + ClassLayout.parseInstance(user).toPrintable());
        }
    });


    thread1.start();
    thread2.start();
    thread1.join();
    thread2.join();
    TimeUnit.SECONDS.sleep(3);
    System.out.println(ClassLayout.parseInstance(user).toPrintable());
}

在上面的代碼中,線程2在啟動(dòng)后休眠兩秒后再嘗試獲取鎖,確保線程1能夠先得到鎖,在此基礎(chǔ)上造成鎖對(duì)象的資源競(jìng)爭(zhēng)。查看對(duì)象鎖狀態(tài)變化:

查看對(duì)象鎖狀態(tài)變化

在線程1持有輕量級(jí)鎖的情況下,線程2嘗試獲取鎖,導(dǎo)致資源競(jìng)爭(zhēng),使輕量級(jí)鎖升級(jí)到重量級(jí)鎖。在兩個(gè)線程都運(yùn)行結(jié)束后,可以看到對(duì)象的狀態(tài)恢復(fù)為了無(wú)鎖不可偏向狀態(tài),在下一次線程嘗試獲取鎖時(shí),會(huì)直接從輕量級(jí)鎖狀態(tài)開(kāi)始。

上面在最后一次打印前將主線程休眠3秒的原因是鎖的釋放過(guò)程需要一定的時(shí)間,如果在線程執(zhí)行完成后直接打印對(duì)象內(nèi)存布局,對(duì)象可能仍處于重量級(jí)鎖狀態(tài)。

3.4 總結(jié)

輕量級(jí)鎖與偏向鎖類似,都是 jdk 對(duì)于多線程的優(yōu)化,不同的是輕量級(jí)鎖是通過(guò) CAS 來(lái)避免開(kāi)銷較大的互斥操作,而偏向鎖是在無(wú)資源競(jìng)爭(zhēng)的情況下完全消除同步。

輕量級(jí)鎖的“輕量”是相對(duì)于重量級(jí)鎖而言的,它的性能會(huì)稍好一些。輕量級(jí)鎖嘗試?yán)?CAS,在升級(jí)為重量級(jí)鎖之前進(jìn)行補(bǔ)救,目的是為了減少多線程進(jìn)入互斥,當(dāng)多個(gè)線程交替執(zhí)行同步塊時(shí),jvm 使用輕量級(jí)鎖來(lái)保證同步,避免線程切換的開(kāi)銷,不會(huì)造成用戶態(tài)與內(nèi)核態(tài)的切換。但是如果過(guò)度自旋,會(huì)引起 cpu 資源的浪費(fèi),這種情況下輕量級(jí)鎖消耗的資源可能反而會(huì)更多。

4 重量級(jí)鎖

4.1 Monitor

重量級(jí)鎖是依賴對(duì)象內(nèi)部的 monitor(監(jiān)視器/管程)來(lái)實(shí)現(xiàn)的 ,而 monitor 又依賴于操作系統(tǒng)底層的Mutex Lock(互斥鎖)實(shí)現(xiàn),這也就是為什么說(shuō)重量級(jí)鎖比較“重”的原因了,操作系統(tǒng)在實(shí)現(xiàn)線程之間的切換時(shí),需要從用戶態(tài)切換到內(nèi)核態(tài),成本非常高。在學(xué)習(xí)重量級(jí)鎖的工作原理前,首先需要了解一下 monitor 中的核心概念:

  • owner:標(biāo)識(shí)擁有該monitor的線程,初始時(shí)和鎖被釋放后都為 null
  • cxq (ConnectionList):競(jìng)爭(zhēng)隊(duì)列,所有競(jìng)爭(zhēng)鎖的線程都會(huì)首先被放入這個(gè)隊(duì)列中
  • EntryList:候選者列表,當(dāng)owner解鎖時(shí)會(huì)將cxq隊(duì)列中的線程移動(dòng)到該隊(duì)列中
  • OnDeck:在將線程從cxq移動(dòng)到EntryList時(shí),會(huì)指定某個(gè)線程為Ready狀態(tài)(即OnDeck),表明它可以競(jìng)爭(zhēng)鎖,如果競(jìng)爭(zhēng)成功那么稱為owner線程,如果失敗則放回EntryList
  • WaitSet:因?yàn)檎{(diào)用wait()wait(time)方法而被阻塞的線程會(huì)被放在該隊(duì)列中
  • count:monitor的計(jì)數(shù)器,數(shù)值加1表示當(dāng)前對(duì)象的鎖被一個(gè)線程獲取,線程釋放monitor對(duì)象時(shí)減1
  • recursions:線程重入次數(shù)

用圖來(lái)表示線程競(jìng)爭(zhēng)的的過(guò)程:

用圖來(lái)表示線程競(jìng)爭(zhēng)的的過(guò)程

當(dāng)線程調(diào)用wait()方法,將釋放當(dāng)前持有的monitor,將owner置為null,進(jìn)入WaitSet集合中等待被喚醒。當(dāng)有線程調(diào)用notify()notifyAll()方法時(shí),也會(huì)釋放持有的monitor,并喚醒WaitSet的線程重新參與monitor的競(jìng)爭(zhēng)。

4.2 重量級(jí)鎖原理

當(dāng)升級(jí)為重量級(jí)鎖的情況下,鎖對(duì)象的mark word中的指針不再指向線程棧中的lock record,而是指向堆中與鎖對(duì)象關(guān)聯(lián)的monitor對(duì)象。當(dāng)多個(gè)線程同時(shí)訪問(wèn)同步代碼時(shí),這些線程會(huì)先嘗試獲取當(dāng)前鎖對(duì)象對(duì)應(yīng)的monitor的所有權(quán):

  • 獲取成功,判斷當(dāng)前線程是不是重入,如果是重入那么recursions+1
  • 獲取失敗,當(dāng)前線程會(huì)被阻塞,等待其他線程解鎖后被喚醒,再次競(jìng)爭(zhēng)鎖對(duì)象

在重量級(jí)鎖的情況下,加解鎖的過(guò)程涉及到操作系統(tǒng)的Mutex Lock進(jìn)行互斥操作,線程間的調(diào)度和線程的狀態(tài)變更過(guò)程需要在用戶態(tài)和核心態(tài)之間進(jìn)行切換,會(huì)導(dǎo)致消耗大量的cpu資源,導(dǎo)致性能降低。

總結(jié)

在jdk1.6中,引入了偏向鎖和輕量級(jí)鎖,并使用鎖升級(jí)機(jī)制對(duì)synchronized進(jìn)行了充分的優(yōu)化。其實(shí)除鎖升級(jí)外,還使用了鎖消除、鎖粗化等優(yōu)化手段,所以對(duì)它的認(rèn)識(shí)要脫離“重量級(jí)”這一概念,不要再單純的認(rèn)為它的性能差了。在某些場(chǎng)景下,synchronized的性能甚至已經(jīng)超過(guò)了Lock同步鎖。

盡管java對(duì)synchronized做了這些優(yōu)化,但是在使用過(guò)程中,我們還是要盡量減少鎖的競(jìng)爭(zhēng),通過(guò)減小加鎖粒度和減少同步代碼的執(zhí)行時(shí)間,來(lái)降低鎖競(jìng)爭(zhēng),盡量使鎖維持在偏向鎖和輕量級(jí)鎖的級(jí)別,避免升級(jí)為重量級(jí)鎖,造成性能的損耗。

最后不得不再提一句,在java15中已經(jīng)默認(rèn)禁用了偏向鎖,并棄用所有相關(guān)的命令行選項(xiàng),雖然說(shuō)不確定未來(lái)的LTS版本會(huì)怎樣改動(dòng),但是了解一下偏向鎖的基礎(chǔ)也沒(méi)什么不好的,畢竟你發(fā)任你發(fā),我用java8~

來(lái)源:公眾號(hào) 碼農(nóng)參上 作者:Dr Hydra

0 人點(diǎn)贊