<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          難搞的偏向鎖終于被 Java 移除了

          共 17434字,需瀏覽 35分鐘

           ·

          2022-01-10 23:45

          作者:日拱一兵

          簡(jiǎn)介:外企小小程序員
          他輸出原創(chuàng)的兩個(gè) Slogan 是:

          1. 你有一個(gè)思想,我有一個(gè)思想,我們交換后,一個(gè)人就有了兩個(gè)思想

          2. If you can NOT explain it simply, you do NOT understand it well enough

          來(lái)源:SegmentFault  思否社區(qū)


          背景



          在 JDK1.5 之前,面對(duì) Java 并發(fā)問(wèn)題, synchronized 是一招鮮的解決方案:


          1. 普通同步方法,鎖上當(dāng)前實(shí)例對(duì)象

          2. 靜態(tài)同步方法,鎖上當(dāng)前類 Class 對(duì)象

          3. 同步塊,鎖上括號(hào)里面配置的對(duì)象


          拿同步塊來(lái)舉例:


          public void test(){
            synchronized (object) {
              i++;
            }
          }


          經(jīng)過(guò) javap -v 編譯后的指令如下:



          monitorenter 指令是在編譯后插入到同步代碼塊的開始位置;monitorexit是插入到方法結(jié)束和異常的位置(實(shí)際隱藏了try-finally),每個(gè)對(duì)象都有一個(gè) monitor 與之關(guān)聯(lián),當(dāng)一個(gè)線程執(zhí)行到 monitorenter 指令時(shí),就會(huì)獲得對(duì)象所對(duì)應(yīng)的 monitor 的所有權(quán),也就獲得到了對(duì)象的鎖。


          當(dāng)另外一個(gè)線程執(zhí)行到同步塊的時(shí)候,由于它沒(méi)有對(duì)應(yīng) monitor 的所有權(quán),就會(huì)被阻塞,此時(shí)控制權(quán)只能交給操作系統(tǒng),也就會(huì)從 user mode 切換到 kernel mode, 由操作系統(tǒng)來(lái)負(fù)責(zé)線程間的調(diào)度和線程的狀態(tài)變更, 需要頻繁的在這兩個(gè)模式下切換(上下文轉(zhuǎn)換)。這種有點(diǎn)競(jìng)爭(zhēng)就找內(nèi)核的行為很不好,會(huì)引起很大的開銷,所以大家都叫它重量級(jí)鎖,自然效率也很低,這也就給很多童鞋留下了一個(gè)根深蒂固的印象 —— synchronized關(guān)鍵字相比于其他同步機(jī)制性能不好。


          免費(fèi)的 Java 并發(fā)編程小冊(cè)在此:https://dayarch.top/p/java-concurrency-book.html


          鎖的演變



          來(lái)到 JDK1.6,要怎樣優(yōu)化才能讓鎖變的輕量級(jí)一些?答案就是:


          輕量級(jí)鎖:CPU CAS


          如果 CPU 通過(guò)簡(jiǎn)單的 CAS 能處理加鎖/釋放鎖,這樣就不會(huì)有上下文的切換,較重量級(jí)鎖而言自然就輕了很多。但是當(dāng)競(jìng)爭(zhēng)很激烈,CAS 嘗試再多也是浪費(fèi) CPU,權(quán)衡一下,不如升級(jí)成重量級(jí)鎖,阻塞線程排隊(duì)競(jìng)爭(zhēng),也就有了輕量級(jí)鎖升級(jí)成重量級(jí)鎖的過(guò)程。



          程序員在追求極致的道路上是永無(wú)止境的,HotSpot 的作者經(jīng)過(guò)研究發(fā)現(xiàn),大多數(shù)情況下,鎖不僅不存在多線程競(jìng)爭(zhēng),而且總是由同一個(gè)線程多次獲得,同一個(gè)線程反復(fù)獲取鎖,如果還按照輕量級(jí)鎖的方式獲取鎖(CAS),也是有一定代價(jià)的,如何讓這個(gè)代價(jià)更小一些呢?


          偏向鎖



          偏向鎖實(shí)際就是鎖對(duì)象潛意識(shí)「偏心」同一個(gè)線程來(lái)訪問(wèn),讓鎖對(duì)象記住線程 ID,當(dāng)線程再次獲取鎖時(shí),亮出身份,如果同一個(gè) ID 直接就獲取鎖就好了,是一種 load-and-test 的過(guò)程,相較 CAS 自然又輕量級(jí)了一些。


          可是多線程環(huán)境,也不可能只是同一個(gè)線程一直獲取這個(gè)鎖,其他線程也是要干活的,如果出現(xiàn)多個(gè)線程競(jìng)爭(zhēng)的情況,也就有了偏向鎖升級(jí)的過(guò)程。



          這里可以先思考一下:偏向鎖可以繞過(guò)輕量級(jí)鎖,直接升級(jí)到重量級(jí)鎖嗎?


          都是同一個(gè)鎖對(duì)象,卻有多種鎖狀態(tài),其目的顯而易見:


          占用的資源越少,程序執(zhí)行的速度越快


          偏向鎖,輕量鎖,它倆都不會(huì)調(diào)用系統(tǒng)互斥量(Mutex Lock),只是為了提升性能,多出的兩種鎖的狀態(tài),這樣可以在不同場(chǎng)景下采取最合適的策略,所以可以總結(jié)性的說(shuō):


          • 偏向鎖:無(wú)競(jìng)爭(zhēng)的情況下,只有一個(gè)線程進(jìn)入臨界區(qū),采用偏向鎖

          • 輕量級(jí)鎖:多個(gè)線程可以交替進(jìn)入臨界區(qū),采用輕量級(jí)鎖

          • 重量級(jí)鎖:多線程同時(shí)進(jìn)入臨界區(qū),交給操作系統(tǒng)互斥量來(lái)處理


          到這里,大家應(yīng)該理解了全局大框,但仍然會(huì)有很多疑問(wèn):


          • 鎖對(duì)象是在哪存儲(chǔ)線程 ID 才可以識(shí)別同一個(gè)線程的?

          • 整個(gè)升級(jí)過(guò)程是如何過(guò)渡的?


          想理解這些問(wèn)題,需要先知道 Java 對(duì)象頭的結(jié)構(gòu)


          認(rèn)識(shí)Java對(duì)象頭



          按照常規(guī)理解,識(shí)別線程 ID 需要一組 mapping 映射關(guān)系來(lái)搞定,如果單獨(dú)維護(hù)這個(gè) mapping 關(guān)系又要考慮線程安全的問(wèn)題。奧卡姆剃刀原理,Java 萬(wàn)物皆是對(duì)象,對(duì)象皆可用作鎖,與其單獨(dú)維護(hù)一個(gè) mapping 關(guān)系,不如中心化將鎖的信息維護(hù)在 Java 對(duì)象本身上。


          Java 對(duì)象頭最多由三部分構(gòu)成:


          • MarkWord

          • ClassMetadata Address

          • Array Length (如果對(duì)象是數(shù)組才會(huì)有這部分


          其中 Markword 是保存鎖狀態(tài)的關(guān)鍵,對(duì)象鎖狀態(tài)可以從偏向鎖升級(jí)到輕量級(jí)鎖,再升級(jí)到重量級(jí)鎖,加上初始的無(wú)鎖狀態(tài),可以理解為有 4 種狀態(tài)。想在一個(gè)對(duì)象中表示這么多信息自然就要用位存儲(chǔ),在 64操作系統(tǒng)中,是這樣存儲(chǔ)的(注意顏色標(biāo)記),想看具體注釋的可以看 hotspot(1.8) 源碼文件 path/hotspot/src/share/vm/oops/markOop.hpp 第 30 行。



          有了這些基本信息,接下來(lái)我們就只需要弄清楚,MarkWord 中的鎖信息是怎么變化的


          認(rèn)識(shí)偏向鎖



          單純的看上圖,還是顯得十分抽象,作為程序員的我們最喜歡用代碼說(shuō)話,貼心的 openjdk 官網(wǎng)提供了可以查看對(duì)象內(nèi)存布局的工具 JOL (java object layout):https://search.maven.org/artifact/org.openjdk.jol/jol-core/0.16/jar


          Maven Package


          <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.14</version>
          </dependency>


          Gradle Package


          implementation 'org.openjdk.jol:jol-core:0.14'


          接下來(lái)我們就通過(guò)代碼來(lái)深入了解一下偏向鎖吧


          注意:上圖(從左到右) 代表 高位 -> 低位

          JOL 輸出結(jié)果(從左到右)代表 低位 -> 高位


          來(lái)看測(cè)試代碼


          場(chǎng)景1

              

          public static void main(String[] args) {
                  Object o = new Object();
                  log.info("未進(jìn)入同步塊,MarkWord 為:");
                  log.info(ClassLayout.parseInstance(o).toPrintable());
                  synchronized (o){
                      log.info(("進(jìn)入同步塊,MarkWord 為:"));
                      log.info(ClassLayout.parseInstance(o).toPrintable());
                  }
              }


          來(lái)看輸出結(jié)果:



          上面我們用到的 JOL 版本為 0.14, 帶領(lǐng)大家快速了解一下位具體值,接下來(lái)我們就要用 0.16 版本查看輸出結(jié)果,因?yàn)檫@個(gè)版本給了我們更友好的說(shuō)明,同樣的代碼,來(lái)看輸出結(jié)果:



          看到這個(gè)結(jié)果,你應(yīng)該是有疑問(wèn)的,JDK 1.6 之后默認(rèn)是開啟偏向鎖的,為什么初始化的代碼是無(wú)鎖狀態(tài),進(jìn)入同步塊產(chǎn)生競(jìng)爭(zhēng)就繞過(guò)偏向鎖直接變成輕量級(jí)鎖了呢?


          雖然默認(rèn)開啟了偏向鎖,但是開啟有延遲,大概 4s。原因是 JVM 內(nèi)部的代碼有很多地方用到了synchronized,如果直接開啟偏向,產(chǎn)生競(jìng)爭(zhēng)就要有鎖升級(jí),會(huì)帶來(lái)額外的性能損耗,所以就有了延遲策略。



          我們可以通過(guò)參數(shù) -XX:BiasedLockingStartupDelay=0 將延遲改為0,但是不建議這么做。我們可以通過(guò)一張圖來(lái)理解一下目前的情況:


          場(chǎng)景2


          那我們就代碼延遲 5 秒來(lái)創(chuàng)建對(duì)象,來(lái)看看偏向是否生效

              

          public static void main(String[] args) throws InterruptedException {
                  // 睡眠 5s
                  Thread.sleep(5000);
                  Object o = new Object();
                  log.info("未進(jìn)入同步塊,MarkWord 為:");
                  log.info(ClassLayout.parseInstance(o).toPrintable());
                  synchronized (o){
                      log.info(("進(jìn)入同步塊,MarkWord 為:"));
                      log.info(ClassLayout.parseInstance(o).toPrintable());
                  }
              }


          重新查看運(yùn)行結(jié)果:



          這樣的結(jié)果是符合我們預(yù)期的,但是結(jié)果中的 biasable 狀態(tài),在 MarkWord 表格中并不存在,其實(shí)這是一種匿名偏向狀態(tài),是對(duì)象初始化中,JVM 幫我們做的。


          這樣當(dāng)有線程進(jìn)入同步塊:


          • 可偏向狀態(tài):直接就 CAS 替換 ThreadID,如果成功,就可以獲取偏向鎖了

          • 不可偏向狀態(tài):就會(huì)變成輕量級(jí)鎖


          那問(wèn)題又來(lái)了,現(xiàn)在鎖對(duì)象有具體偏向的線程,如果新的線程過(guò)來(lái)執(zhí)行同步塊會(huì)偏向新的線程嗎?


          場(chǎng)景3

              

          public static void main(String[] args) throws InterruptedException {
                  // 睡眠 5s
                  Thread.sleep(5000);
                  Object o = new Object();
                  log.info("未進(jìn)入同步塊,MarkWord 為:");
                  log.info(ClassLayout.parseInstance(o).toPrintable());
                  synchronized (o){
                      log.info(("進(jìn)入同步塊,MarkWord 為:"));
                      log.info(ClassLayout.parseInstance(o).toPrintable());
                  }

                  Thread t2 = new Thread(() -> {
                      synchronized (o) {
                          log.info("新線程獲取鎖,MarkWord為:");
                          log.info(ClassLayout.parseInstance(o).toPrintable());
                      }
                  });

                  t2.start();
                  t2.join();
                  log.info("主線程再次查看鎖對(duì)象,MarkWord為:");
                  log.info(ClassLayout.parseInstance(o).toPrintable());

                  synchronized (o){
                      log.info(("主線程再次進(jìn)入同步塊,MarkWord 為:"));
                      log.info(ClassLayout.parseInstance(o).toPrintable());
                  }
              }


          來(lái)看運(yùn)行結(jié)果,奇怪的事情發(fā)生了:



          • 標(biāo)記1: 初始可偏向狀態(tài)

          • 標(biāo)記2:偏向主線程后,主線程退出同步代碼塊

          • 標(biāo)記3新線程進(jìn)入同步代碼塊,升級(jí)成了輕量級(jí)鎖

          • 標(biāo)記4: 新線程輕量級(jí)鎖退出同步代碼塊,主線程查看,變?yōu)椴豢善驙顟B(tài)

          • 標(biāo)記5: 由于對(duì)象不可偏向,同場(chǎng)景1主線程再次進(jìn)入同步塊,自然就會(huì)用輕量級(jí)鎖


          至此,場(chǎng)景一二三可以總結(jié)為一張圖:



          從這樣的運(yùn)行結(jié)果上來(lái)看,偏向鎖像是“一錘子買賣”,只要偏向了某個(gè)線程,后續(xù)其他線程嘗試獲取鎖,都會(huì)變?yōu)檩p量級(jí)鎖,這樣的偏向非常有局限性。事實(shí)上并不是這樣,如果你仔細(xì)看標(biāo)記2(已偏向狀態(tài)),還有個(gè) epoch 我們沒(méi)有提及,這個(gè)值就是打破這種局限性的關(guān)鍵,在了解 epoch 之前,我們還要了解一個(gè)概念——偏向撤銷


          免費(fèi)的 Java 并發(fā)編程小冊(cè)在此:https://dayarch.top/p/java-concurrency-book.html


          偏向撤銷


          在真正講解偏向撤銷之前,需要和大家明確一個(gè)概念——偏向鎖撤銷和偏向鎖釋放是兩碼事。


          • 撤銷:籠統(tǒng)的說(shuō)就是多個(gè)線程競(jìng)爭(zhēng)導(dǎo)致不能再使用偏向模式的時(shí)候,主要是告知這個(gè)鎖對(duì)象不能再用偏向模式

          • 釋放:和你的常規(guī)理解一樣,對(duì)應(yīng)的就是 synchronized 方法的退出或 synchronized 塊的結(jié)束


          何為偏向撤銷?


          從偏向狀態(tài)撤回原有的狀態(tài),也就是將 MarkWord 的第 3 位(是否偏向撤銷)的值,從 1 變回 0


          如果只是一個(gè)線程獲取鎖,再加上「偏心」的機(jī)制,是沒(méi)有理由撤銷偏向的,所以偏向的撤銷只能發(fā)生在有競(jìng)爭(zhēng)的情況下。


          想要撤銷偏向鎖,還不能對(duì)持有偏向鎖的線程有影響,所以就要等待持有偏向鎖的線程到達(dá)一個(gè) safepoint 安全點(diǎn) (這里的安全點(diǎn)是 JVM 為了保證在垃圾回收的過(guò)程中引用關(guān)系不會(huì)發(fā)生變化設(shè)置的一種安全狀態(tài),在這個(gè)狀態(tài)上會(huì)暫停所有線程工作), 在這個(gè)安全點(diǎn)會(huì)掛起獲得偏向鎖的線程。


          在這個(gè)安全點(diǎn),線程可能還是處在不同狀態(tài)的,先說(shuō)結(jié)論(因?yàn)樵创a就是這么寫的,可能有疑惑的地方會(huì)在后面解釋)


          • 線程不存活或者活著的線程但退出了同步塊,很簡(jiǎn)單,直接撤銷偏向就好了

          • 活著的線程但仍在同步塊之內(nèi),那就要升級(jí)成輕量級(jí)鎖


          這個(gè)和 epoch 貌似還是沒(méi)啥關(guān)系,因?yàn)檫@還不是全部場(chǎng)景。偏向鎖是特定場(chǎng)景下提升程序效率的方案,可并不代表程序員寫的程序都滿足這些特定場(chǎng)景,比如這些場(chǎng)景(在開啟偏向鎖的前提下):


          • 一個(gè)線程創(chuàng)建了大量對(duì)象并執(zhí)行了初始的同步操作,之后在另一個(gè)線程中將這些對(duì)象作為鎖進(jìn)行之后的操作。這種case下,會(huì)導(dǎo)致大量的偏向鎖撤銷操作

          • 明知有多線程競(jìng)爭(zhēng)(生產(chǎn)者/消費(fèi)者隊(duì)列),還要使用偏向鎖,也會(huì)導(dǎo)致各種撤銷


          很顯然,這兩種場(chǎng)景肯定會(huì)導(dǎo)致偏向撤銷的,一個(gè)偏向撤銷的成本無(wú)所謂,大量偏向撤銷的成本是不能忽視的。那怎么辦?既不想禁用偏向鎖,還不想忍受大量撤銷偏向增加的成本,這種方案就是設(shè)計(jì)一個(gè)有階梯的底線。


          批量重偏向(bulk rebias)


          這是第一種場(chǎng)景的快速解決方案,以 class 為單位,為每個(gè) class 維護(hù)一個(gè)偏向鎖撤銷計(jì)數(shù)器,每一次該class的對(duì)象發(fā)生偏向撤銷操作時(shí),該計(jì)數(shù)器 +1,當(dāng)這個(gè)值達(dá)到重偏向閾值(默認(rèn)20)時(shí):


          BiasedLockingBulkRebiasThreshold = 20


          JVM 就認(rèn)為該class的偏向鎖有問(wèn)題,因此會(huì)進(jìn)行批量重偏向, 它的實(shí)現(xiàn)方式就用到了我們上面說(shuō)的 epoch


          Epoch,如其含義「紀(jì)元」一樣,就是一個(gè)時(shí)間戳。每個(gè) class 對(duì)象會(huì)有一個(gè)對(duì)應(yīng)的epoch字段,每個(gè)處于偏向鎖狀態(tài)對(duì)象mark word 中也有該字段,其初始值為創(chuàng)建該對(duì)象時(shí) class 中的epoch的值(此時(shí)二者是相等的)。每次發(fā)生批量重偏向時(shí),就將該值加1,同時(shí)遍歷JVM中所有線程的棧。


          • 找到該 class 所有正處于加鎖狀態(tài)的偏向鎖對(duì)象,將其epoch字段改為新值

          • class 中不處于加鎖狀態(tài)的偏向鎖對(duì)象(沒(méi)被任何線程持有,但之前是被線程持有過(guò)的,這種鎖對(duì)象的 markword 肯定也是有偏向的),保持 epoch 字段值不變


          這樣下次獲得鎖時(shí),發(fā)現(xiàn)當(dāng)前對(duì)象的epoch值和class的epoch,本著今朝不問(wèn)前朝事 的原則(上一個(gè)紀(jì)元),那就算當(dāng)前已經(jīng)偏向了其他線程,也不會(huì)執(zhí)行撤銷操作,而是直接通過(guò) CAS 操作將其mark word的線程 ID 改成當(dāng)前線程 ID,這也算是一定程度的優(yōu)化,畢竟沒(méi)升級(jí)鎖;


          如果 epoch 都一樣,說(shuō)明沒(méi)有發(fā)生過(guò)批量重偏向, 如果 markword 有線程ID,還有其他鎖來(lái)競(jìng)爭(zhēng),那鎖自然是要升級(jí)的(如同前面舉的例子 epoch=0)


          批量重偏向是第一階梯底線,還有第二階梯底線


          批量撤銷(bulk revoke)


          當(dāng)達(dá)到重偏向閾值后,假設(shè)該 class 計(jì)數(shù)器繼續(xù)增長(zhǎng),當(dāng)其達(dá)到批量撤銷的閾值后(默認(rèn)40)時(shí),


          BiasedLockingBulkRevokeThreshold = 40


          JVM就認(rèn)為該 class 的使用場(chǎng)景存在多線程競(jìng)爭(zhēng),會(huì)標(biāo)記該 class 為不可偏向。之后對(duì)于該 class 的鎖,直接走輕量級(jí)鎖的邏輯。


          這就是第二階梯底線,但是在第一階梯到第二階梯的過(guò)渡過(guò)程中,也就是在徹底禁用偏向鎖之前,還給一次改過(guò)自新的機(jī)會(huì),那就是另外一個(gè)計(jì)時(shí)器:


          BiasedLockingDecayTime = 25000


          • 如果在距離上次批量重偏向發(fā)生的 25 秒之內(nèi),并且累計(jì)撤銷計(jì)數(shù)達(dá)到40,就會(huì)發(fā)生批量撤銷(偏向鎖徹底 game over)

          • 如果在距離上次批量重偏向發(fā)生超過(guò) 25 秒之外,那么就會(huì)重置在 [20, 40) 內(nèi)的計(jì)數(shù), 再給次機(jī)會(huì)


          大家有興趣可以寫代碼測(cè)試一下臨界點(diǎn),觀察鎖對(duì)象 markword 的變化。


          至此,整個(gè)偏向鎖的工作流程可以用一張圖表示:



          到此,你應(yīng)該對(duì)偏向鎖有個(gè)基本的認(rèn)識(shí)了,但是我心中的好多疑問(wèn)還沒(méi)有解除,咱們繼續(xù)看:


          HashCode 哪去了


          上面場(chǎng)景一,無(wú)鎖狀態(tài),對(duì)象頭中沒(méi)有 hashcode;偏向鎖狀態(tài),對(duì)象頭還是沒(méi)有 hashcode,那我們的 hashcode 哪去了?


          首先要知道,hashcode 不是創(chuàng)建對(duì)象就幫我們寫到對(duì)象頭中的,而是要經(jīng)過(guò)第一次調(diào)用 Object::hashCode() 或者System::identityHashCode(Object) 才會(huì)存儲(chǔ)在對(duì)象頭中的。第一次生成的 hashcode后,該值應(yīng)該是一直保持不變的,但偏向鎖又是來(lái)回更改鎖對(duì)象的 markword,必定會(huì)對(duì) hashcode 的生成有影響,那怎么辦呢?,我們來(lái)用代碼驗(yàn)證:


          場(chǎng)景一

              

          public static void main(String[] args) throws InterruptedException {
                  // 睡眠 5s
                  Thread.sleep(5000);

                  Object o = new Object();
                  log.info("未生成 hashcode,MarkWord 為:");
                  log.info(ClassLayout.parseInstance(o).toPrintable());

                  o.hashCode();
                  log.info("已生成 hashcode,MarkWord 為:");
                  log.info(ClassLayout.parseInstance(o).toPrintable());

                  synchronized (o){
                      log.info(("進(jìn)入同步塊,MarkWord 為:"));
                      log.info(ClassLayout.parseInstance(o).toPrintable());
                  }
              }


          來(lái)看運(yùn)行結(jié)果



          結(jié)論就是:即便初始化為可偏向狀態(tài)的對(duì)象,一旦調(diào)用 Object::hashCode() 或者System::identityHashCode(Object) ,進(jìn)入同步塊就會(huì)直接使用輕量級(jí)鎖


          場(chǎng)景二


          假如已偏向某一個(gè)線程,然后生成 hashcode,然后同一個(gè)線程又進(jìn)入同步塊,會(huì)發(fā)生什么呢?來(lái)看代碼:

              

          public static void main(String[] args) throws InterruptedException {
                  // 睡眠 5s
                  Thread.sleep(5000);

                  Object o = new Object();
                  log.info("未生成 hashcode,MarkWord 為:");
                  log.info(ClassLayout.parseInstance(o).toPrintable());

                  synchronized (o){
                      log.info(("進(jìn)入同步塊,MarkWord 為:"));
                      log.info(ClassLayout.parseInstance(o).toPrintable());
                  }

                  o.hashCode();
                  log.info("生成 hashcode");
                  synchronized (o){
                      log.info(("同一線程再次進(jìn)入同步塊,MarkWord 為:"));
                      log.info(ClassLayout.parseInstance(o).toPrintable());
                  }
              }


          查看運(yùn)行結(jié)果:



          結(jié)論就是:同場(chǎng)景一,會(huì)直接使用輕量級(jí)鎖


          場(chǎng)景三


          那假如對(duì)象處于已偏向狀態(tài),在同步塊中調(diào)用了那兩個(gè)方法會(huì)發(fā)生什么呢?繼續(xù)代碼驗(yàn)證:

              

          public static void main(String[] args) throws InterruptedException {
                  // 睡眠 5s
                  Thread.sleep(5000);

                  Object o = new Object();
                  log.info("未生成 hashcode,MarkWord 為:");
                  log.info(ClassLayout.parseInstance(o).toPrintable());

                  synchronized (o){
                      log.info(("進(jìn)入同步塊,MarkWord 為:"));
                      log.info(ClassLayout.parseInstance(o).toPrintable());
                      o.hashCode();
                      log.info("已偏向狀態(tài)下,生成 hashcode,MarkWord 為:");
                      log.info(ClassLayout.parseInstance(o).toPrintable());
                  }
              }


          來(lái)看運(yùn)行結(jié)果:



          結(jié)論就是:如果對(duì)象處在已偏向狀態(tài),生成 hashcode 后,就會(huì)直接升級(jí)成重量級(jí)鎖。


          最后用書中的一段話來(lái)描述 鎖和hashcode 之前的關(guān)系。



          調(diào)用 Object.wait() 方法會(huì)發(fā)生什么?


          Object 除了提供了上述 hashcode 方法,還有 wait() 方法,這也是我們?cè)谕綁K中常用的,那這會(huì)對(duì)鎖產(chǎn)生哪些影響呢?來(lái)看代碼:

              

          public static void main(String[] args) throws InterruptedException {
                  // 睡眠 5s
                  Thread.sleep(5000);

                  Object o = new Object();
                  log.info("未生成 hashcode,MarkWord 為:");
                  log.info(ClassLayout.parseInstance(o).toPrintable());

                  synchronized (o) {
                      log.info(("進(jìn)入同步塊,MarkWord 為:"));
                      log.info(ClassLayout.parseInstance(o).toPrintable());

                      log.info("wait 2s");
                      o.wait(2000);

                      log.info(("調(diào)用 wait 后,MarkWord 為:"));
                      log.info(ClassLayout.parseInstance(o).toPrintable());
                  }
              }


          查看運(yùn)行結(jié)果:



          結(jié)論就是,wait 方法是互斥量(重量級(jí)鎖)獨(dú)有的,一旦調(diào)用該方法,就會(huì)升級(jí)成重量級(jí)鎖(這個(gè)是面試可以說(shuō)出的亮點(diǎn)內(nèi)容哦)


          最后再繼續(xù)豐富一下鎖對(duì)象變化圖:




          免費(fèi)的 Java 并發(fā)編程小冊(cè)在此:https://dayarch.top/p/java-concurrency-book.html


          告別偏向鎖



          看到這個(gè)標(biāo)題你應(yīng)該是有些慌,為啥要告別偏向鎖,因?yàn)榫S護(hù)成本有些高了,來(lái)看 Open JDK 官方聲明,JEP 374: Deprecate and Disable Biased Locking,相信你看上面的文字說(shuō)明也深有體會(huì)。


          聲明鏈接:https://openjdk.java.net/jeps/374



          這個(gè)說(shuō)明的更新時(shí)間距離現(xiàn)在很近,在 JDK15 版本就已經(jīng)開始了



          一句話解釋就是維護(hù)成本太高




          最終就是,JDK 15 之前,偏向鎖默認(rèn)是 enabled,從 15 開始,默認(rèn)就是 disabled,除非顯示的通過(guò) UseBiasedLocking 開啟


          其中在 quarkus 上的一篇文章說(shuō)明的更加直接



          偏向鎖給 JVM 增加了巨大的復(fù)雜性,只有少數(shù)非常有經(jīng)驗(yàn)的程序員才能理解整個(gè)過(guò)程,維護(hù)成本很高,大大阻礙了開發(fā)新特性的進(jìn)程(換個(gè)角度理解,你掌握了,是不是就是那少數(shù)有經(jīng)驗(yàn)的程序員了呢?哈哈)


          總結(jié)



          偏向鎖可能就這樣的走完了它的一生,有些同學(xué)可能直接發(fā)問(wèn),都被 deprecated 了,JDK都 17 了,還講這么多干什么?


          • java 任它發(fā),我用 Java8,這是很多主流的狀態(tài),至少你用的版本沒(méi)有被 deprecated

          • 面試還是會(huì)被經(jīng)常問(wèn)到

          • 萬(wàn)一哪天有更好的設(shè)計(jì)方案,“偏向鎖”又以新的形式回來(lái)了呢,了解變化才能更好理解背后設(shè)計(jì)

          • 奧卡姆剃刀原理,我們現(xiàn)實(shí)中的優(yōu)化也一樣,如果沒(méi)有必要不要增加實(shí)體,如果增加的內(nèi)容帶來(lái)很大的成本,不如大膽的廢除掉,接受一點(diǎn)落差


          之前對(duì)于偏向鎖我也只是單純的理論認(rèn)知,但是為了寫這篇文章,我翻閱了很多資料,包括也重新查看 Hotspot 源碼,說(shuō)的這些內(nèi)容也并不能完全說(shuō)明偏向鎖的整個(gè)流程細(xì)節(jié),還需要大家具體實(shí)踐追蹤查看,這里給出源碼的幾個(gè)關(guān)鍵入口,方便大家追蹤:


          • 偏向鎖入口: 

            http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/9ce27f0a4683/src/share/vm/interpreter/bytecodeInterpreter.cpp#l1816

          • 偏向撤銷入口:

            http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/9ce27f0a4683/src/share/vm/interpreter/interpreterRuntime.cpp#l608

          • 偏向鎖釋放入口:

            http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/9ce27f0a4683/src/share/vm/interpreter/bytecodeInterpreter.cpp#l1923


          文中有疑問(wèn)的地方歡迎留言討論,有錯(cuò)誤的地方還請(qǐng)大家?guī)兔χ刚?/span>


          靈魂追問(wèn)



          輕量級(jí)和重量級(jí)鎖,hashcode 存在了什么位置?


          參考資料



          感謝各路前輩的精華總結(jié),可以讓我參考理解:

          • https://www.oracle.com/technetwork/java/javase/tech/biasedlocking-oopsla2006-preso-150106.pdf

          • https://www.oracle.com/technetwork/java/biasedlocking-oopsla2006-wp-149958.pdf

          • https://wiki.openjdk.java.net/display/HotSpot/Synchronization#Synchronization-Russel06

          • https://github.com/farmerjohngit/myblog/issues/12

          • https://zhuanlan.zhihu.com/p/440994983

          • https://mp.weixin.qq.com/s/G4z08HfiqJ4qm3th0KtovA

          • https://www.jianshu.com/p/884eb51266e4



          點(diǎn)擊左下角閱讀原文,到 SegmentFault 思否社區(qū) 和文章作者展開更多互動(dòng)和交流,掃描下方”二維碼“或在“公眾號(hào)后臺(tái)回復(fù)“ 入群 ”即可加入我們的技術(shù)交流群,收獲更多的技術(shù)文章~

          - END -


          瀏覽 37
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  91影院理论推荐手机在线观看 | 一区二区无码区 | 国产乱伦视频网站 | 好逼123 | 4080yy午夜理论片成人 |