<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>

          詳解synchronized和鎖升級,以及偏向鎖和輕量級鎖的升級

          共 11340字,需瀏覽 23分鐘

           ·

          2021-06-11 17:21

          你知道的越多,不知道的就越多,業(yè)余的像一棵小草!

          成功路上并不擁擠,因為堅持的人不多。

          編輯:業(yè)余草

          blog.csdn.net/tongdanping

          推薦:https://www.xttblog.com/?p=5190

          Synchronized 使用場景

          synchronized 是一個同步關鍵字,在某些多線程場景下,如果不進行同步會導致數(shù)據(jù)不安全,而 synchronized 關鍵字就是用于代碼同步。什么情況下會數(shù)據(jù)不安全呢,要滿足兩個條件:一是數(shù)據(jù)共享(臨界資源),二是多線程同時訪問并改變該數(shù)據(jù)。

          例如:

          public class AccountingSync implements Runnable{
              //共享資源(臨界資源)
              static int i=0;

              /**
               * synchronized 修飾實例方法
               */

              public synchronized void increase(){
                  i++;
              }
              @Override
              public void run() {
                  for(int j=0;j<1000000;j++){
                      increase();
                  }
              }
              public static void main(String[] args) throws InterruptedException {
                  AccountingSync instance=new AccountingSync();
                  Thread t1=new Thread(instance);
                  Thread t2=new Thread(instance);
                  t1.start();
                  t2.start();
                  t1.join();
                  t2.join();
                  System.out.println(i);
              }
          }

          該段程序的輸出為:2000000

          但是如果 increase 的 synchronized 被刪除,那么很可能輸出結(jié)果就會小于 2000000,這是因為多個線程同時訪問臨界資源 i,如果一個線程 A 對 i=88 的自增到 89 沒有被 B 線程讀取到,線程 B 認為 i 仍然是 88,那么線程 B 對 i 的自增結(jié)果還是 89,那么這里就會出現(xiàn)問題。

          synchronized 鎖的 3 種使用形式(使用場景):

          • synchronized 修飾普通同步方法:鎖對象當前實例對象;
          • synchronized 修飾靜態(tài)同步方法:鎖對象是當前的類 Class 對象;
          • synchronized 修飾同步代碼塊:鎖對象是 synchronized 后面括號里配置的對象,這個對象可以是某個對象(xlock),也可以是某個類(Xlock.class);

          注意:

          • 使用 synchronized 修飾非靜態(tài)方法或者使用 synchronized 修飾代碼塊時制定的為實例對象時,同一個類的不同對象擁有自己的鎖,因此不會相互阻塞。
          • 使用 synchronized 修飾類和對象時,由于類對象和實例對象分別擁有自己的監(jiān)視器鎖,因此不會相互阻塞。
          • 使用使用 synchronized 修飾實例對象時,如果一個線程正在訪問實例對象的一個 synchronized 方法時,其它線程不僅不能訪問該 synchronized 方法,該對象的其它 synchronized 方法也不能訪問,因為一個對象只有一個監(jiān)視器鎖對象,但是其它線程可以訪問該對象的非 synchronized 方法。
          • 線程 A 訪問實例對象的非 static synchronized 方法時,線程 B 也可以同時訪問實例對象的 static synchronized 方法,因為前者獲取的是實例對象的監(jiān)視器鎖,而后者獲取的是類對象的監(jiān)視器鎖,兩者不存在互斥關系。

          synchronized 實現(xiàn)原理

          Java 對象頭

          首先,我們要知道對象在內(nèi)存中的布局:

          已知對象是存放在堆內(nèi)存中的,對象大致可以分為三個部分,分別是對象頭、實例變量和填充字節(jié)。

          • 對象頭的主要是由 MarkWord 和Klass Point(類型指針)組成,其中 Klass Point 是是對象指向它的類元數(shù)據(jù)的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例,Mark Word 用于存儲對象自身的運行時數(shù)據(jù)。如果對象是數(shù)組對象,那么對象頭占用 3 個字寬(Word),如果對象是非數(shù)組對象,那么對象頭占用 2 個字寬。(1word = 2 Byte = 16 bit)
          • 實例變量存儲的是對象的屬性信息,包括父類的屬性信息,按照 4 字節(jié)對齊
          • 填充字符,因為虛擬機要求對象字節(jié)必須是 8 字節(jié)的整數(shù)倍,填充字符就是用于湊齊這個整數(shù)倍的

          通過第一部分可以知道,synchronized 不論是修飾方法還是代碼塊,都是通過持有修飾對象的鎖來實現(xiàn)同步,那么 synchronized 鎖對象是存在哪里的呢?答案是存在鎖對象的對象頭的 MarkWord 中。那么 MarkWord 在對象頭中到底長什么樣,也就是它到底存儲了什么呢?

          在 32 位的虛擬機中:

          在 64 位的虛擬機中:

          上圖中的偏向鎖和輕量級鎖都是在 java6以后對鎖機制進行優(yōu)化時引進的,下文的鎖升級部分會具體講解,synchronized 關鍵字對應的是重量級鎖,接下來對重量級鎖在 Hotspot JVM 中的實現(xiàn)鎖講解。

          synchronized 在 JVM 中的實現(xiàn)原理

          重量級鎖對應的鎖標志位是 10,存儲了指向重量級監(jiān)視器鎖的指針,在 Hotspot中,對象的監(jiān)視器(monitor)鎖對象由 ObjectMonitor 對象實現(xiàn)(C++),其跟同步相關的數(shù)據(jù)結(jié)構(gòu)如下:

          ObjectMonitor() {
              _count        = 0//用來記錄該對象被線程獲取鎖的次數(shù)
              _waiters      = 0;
              _recursions   = 0//鎖的重入次數(shù)
              _owner        = NULL; //指向持有ObjectMonitor對象的線程 
              _WaitSet      = NULL; //處于wait狀態(tài)的線程,會被加入到_WaitSet
              _WaitSetLock  = 0 ;
              _EntryList    = NULL ; //處于等待鎖block狀態(tài)的線程,會被加入到該列表
            }

          光看這些數(shù)據(jù)結(jié)構(gòu)對監(jiān)視器鎖的工作機制還是一頭霧水,那么我們首先看一下線程在獲取鎖的幾個狀態(tài)的轉(zhuǎn)換:

          線程的生命周期存在 5 個狀態(tài),start、running、waiting、blocking 和 dead

          對于一個 synchronized 修飾的方法(代碼塊)來說:

          1. 當多個線程同時訪問該方法,那么這些線程會先被放進 _EntryList 隊列,此時線程處于 blocking 狀態(tài)
          2. 當一個線程獲取到了實例對象的監(jiān)視器(monitor)鎖,那么就可以進入running 狀態(tài),執(zhí)行方法,此時,ObjectMonitor 對象的 _owner 指向當前線程,_count 加 1 表示當前對象鎖被一個線程獲取
          3. 當 running 狀態(tài)的線程調(diào)用 wait() 方法,那么當前線程釋放 monitor 對象,進入 waiting 狀態(tài),ObjectMonitor 對象的 _owner 變?yōu)?null,_count 減 1,同時線程進入 _WaitSet 隊列,直到有線程調(diào)用 notify() 方法喚醒該線程,則該線程重新獲取 monitor 對象進入 _Owner 區(qū)
          4. 如果當前線程執(zhí)行完畢,那么也釋放 monitor 對象,進入 waiting 狀態(tài), ObjectMonitor 對象的 _owner變?yōu)?null,_count 減 1

          「那么 synchronized 修飾的代碼塊/方法如何獲取 monitor 對象的呢?」

          在 JVM 規(guī)范里可以看到,不管是方法同步還是代碼塊同步都是基于進入和退出monitor 對象來實現(xiàn),然而二者在具體實現(xiàn)上又存在很大的區(qū)別。通過 javap 對 class 字節(jié)碼文件反編譯可以得到反編譯后的代碼。

          synchronized修飾代碼塊

          synchronized 代碼塊同步在需要同步的代碼塊開始的位置插入 monitorentry 指令,在同步結(jié)束的位置或者異常出現(xiàn)的位置插入 monitorexit 指令;JVM 要保證 monitorentry 和 monitorexit 都是成對出現(xiàn)的,任何對象都有一個 monitor 與之對應,當這個對象的 monitor 被持有以后,它將處于鎖定狀態(tài)。

          例如,同步代碼塊如下:

          public class SyncCodeBlock {
             public int i;
             public void syncTask(){
                 synchronized (this){
                     i++;
                 }
             }
          }

          對同步代碼塊編譯后的 class 字節(jié)碼文件反編譯,結(jié)果如下(僅保留方法部分的反編譯內(nèi)容):

          public void syncTask();
              descriptor: ()V
              flags: ACC_PUBLIC
              Code:
                stack=3, locals=3, args_size=1
                   0: aload_0
                   1: dup
                   2: astore_1
                   3: monitorenter  //注意此處,進入同步方法
                   4: aload_0
                   5: dup
                   6: getfield      #2             // Field i:I
                   9: iconst_1
                  10: iadd
                  11: putfield      #2            // Field i:I
                  14: aload_1
                  15: monitorexit   //注意此處,退出同步方法
                  16: goto          24
                  19: astore_2
                  20: aload_1
                  21: monitorexit //注意此處,退出同步方法
                  22: aload_2
                  23: athrow
                  24return
                Exception table:
                //省略其他字節(jié)碼.......

          可以看出同步方法塊在進入代碼塊時插入了 monitorentry 語句,在退出代碼塊時插入了 monitorexit 語句,為了保證不論是正常執(zhí)行完畢(第 15 行)還是異常跳出代碼塊(第 21 行)都能執(zhí)行 monitorexit 語句,因此會出現(xiàn)兩句 monitorexit 語句。

          synchronized 修飾方法

          synchronized 方法同步不再是通過插入 monitorentry 和 monitorexit 指令實現(xiàn),而是由方法調(diào)用指令來讀取運行時常量池中的 ACC_SYNCHRONIZED 標志隱式實現(xiàn)的,如果方法表結(jié)構(gòu)(method_info Structure)中的 ACC_SYNCHRONIZED 標志被設置,那么線程在執(zhí)行方法前會先去獲取對象的 monitor 對象,如果獲取成功則執(zhí)行方法代碼,執(zhí)行完畢后釋放 monitor 對象,如果 monitor 對象已經(jīng)被其它線程獲取,那么當前線程被阻塞。

          同步方法代碼如下:

          public class SyncMethod {
             public int i;
             public synchronized void syncTask(){
                     i++;
             }
          }

          對同步方法編譯后的 class 字節(jié)碼反編譯,結(jié)果如下(僅保留方法部分的反編譯內(nèi)容):

          public synchronized void syncTask();
              descriptor: ()V
              //方法標識ACC_PUBLIC代表public修飾,ACC_SYNCHRONIZED指明該方法為同步方法
              flags: ACC_PUBLIC, ACC_SYNCHRONIZED
              Code:
                stack=3, locals=1, args_size=1
                   0: aload_0
                   1: dup
                   2: getfield      #2                  // Field i:I
                   5: iconst_1
                   6: iadd
                   7: putfield      #2                  // Field i:I
                  10return
                LineNumberTable:
                  line 120
                  line 1310
          }

          可以看出方法開始和結(jié)束的地方都沒有出現(xiàn) monitorentry 和 monitorexit 指令,但是出現(xiàn)的 ACC_SYNCHRONIZED 標志位。

          鎖的優(yōu)化

          鎖的 4 中狀態(tài):無鎖狀態(tài)、偏向鎖狀態(tài)、輕量級鎖狀態(tài)、重量級鎖狀態(tài)(級別從低到高),整個鎖的狀態(tài)從低到高變化的過程被稱為所升級。

          為什么要引入偏向鎖?

          因為經(jīng)過 HotSpot 的作者大量的研究發(fā)現(xiàn),大多數(shù)時候是不存在鎖競爭的,常常是一個線程多次獲得同一個鎖,因此如果每次都要競爭鎖會增大很多沒有必要付出的代價,為了降低獲取鎖的代價,才引入的偏向鎖。

          偏向鎖的升級

          當線程 1 訪問代碼塊并獲取鎖對象時,會在 java 對象頭和棧幀中記錄偏向的鎖的 threadID,因為「偏向鎖不會主動釋放鎖」,因此以后線程1再次獲取鎖的時候,需要「比較當前線程的 threadID 和 Java 對象頭中的 threadID 是否一致」,如果一致(還是線程 1 獲取鎖對象),則無需使用 CAS 來加鎖、解鎖;如果不一致(其他線程,如線程 2 要競爭鎖對象,而偏向鎖不會主動釋放因此還是存儲的線程 1 的 threadID),那么需要「查看 Java 對象頭中記錄的線程 1 是否存活」,如果沒有存活,那么鎖對象被重置為無鎖狀態(tài),其它線程(線程 2)可以競爭將其設置為偏向鎖;如果存活,那么立刻「查找該線程(線程 1)的棧幀信息,如果還是需要繼續(xù)持有這個鎖對象」,那么暫停當前線程 1,撤銷偏向鎖,升級為輕量級鎖,如果線程 1 不再使用該鎖對象,那么將鎖對象狀態(tài)設為無鎖狀態(tài),重新偏向新的線程。

          偏向鎖的取消

          偏向鎖是默認開啟的,而且開始時間一般是比應用程序啟動慢幾秒,如果不想有這個延遲,那么可以使用 -XX:BiasedLockingStartUpDelay=0;

          如果不想要偏向鎖,那么可以通過 -XX:-UseBiasedLocking = false 來設置;

          輕量級鎖

          為什么要引入輕量級鎖?

          輕量級鎖考慮的是競爭鎖對象的線程不多,而且線程持有鎖的時間也不長的情景。因為阻塞線程需要 CPU 從用戶態(tài)轉(zhuǎn)到內(nèi)核態(tài),代價較大,如果剛剛阻塞不久這個鎖就被釋放了,那這個代價就有點得不償失了,因此這個時候就干脆不阻塞這個線程,讓它自旋這等待鎖釋放。

          輕量級鎖什么時候升級為重量級鎖?

          線程 1 獲取輕量級鎖時會先把鎖對象的「對象頭 MarkWord 復制一份到線程 1 的棧幀中創(chuàng)建的用于存儲鎖記錄的空間」(稱為 DisplacedMarkWord),然后「使用 CAS 把對象頭中的內(nèi)容替換為線程 1 存儲的鎖記錄(「DisplacedMarkWord」)的地址」

          如果在線程 1 復制對象頭的同時(在線程 1 CAS之前),線程 2 也準備獲取鎖,復制了對象頭到線程 2 的鎖記錄空間中,但是在線程 2 CAS 的時候,發(fā)現(xiàn)線程 1 已經(jīng)把對象頭換了,「線程 2 的 CAS 失敗,那么線程 2 就嘗試使用自旋鎖來等待線程 1 釋放鎖」

          但是如果自旋的時間太長也不行,因為自旋是要消耗 CPU 的,因此自旋的次數(shù)是有限制的,比如 10 次或者 100 次,如果「自旋次數(shù)到了線程 1 還沒有釋放鎖,或者線程 1 還在執(zhí)行,線程 2 還在自旋等待,這時又有一個線程 3 過來競爭這個鎖對象,那么這個時候輕量級鎖就會膨脹為重量級鎖。重量級鎖把除了擁有鎖的線程都阻塞,防止 CPU 空轉(zhuǎn)。」

          ?

          注意:為了避免無用的自旋,輕量級鎖一旦膨脹為重量級鎖就不會再降級為輕量級鎖了;偏向鎖升級為輕量級鎖也不能再降級為偏向鎖。一句話就是鎖可以升級不可以降級,但是偏向鎖狀態(tài)可以被重置為無鎖狀態(tài)。

          ?

          這幾種鎖的優(yōu)缺點(偏向鎖、輕量級鎖、重量級鎖)

          鎖粗化

          按理來說,同步塊的作用范圍應該盡可能小,僅在共享數(shù)據(jù)的實際作用域中才進行同步,這樣做的目的是為了使需要同步的操作數(shù)量盡可能縮小,縮短阻塞時間,如果存在鎖競爭,那么等待鎖的線程也能盡快拿到鎖。

          但是加鎖解鎖也需要消耗資源,如果存在一系列的連續(xù)加鎖解鎖操作,可能會導致不必要的性能損耗。

          鎖粗化就是將多個連續(xù)的加鎖、解鎖操作連接在一起,擴展成一個范圍更大的鎖,避免頻繁的加鎖解鎖操作。

          鎖消除

          Java 虛擬機在 JIT 編譯時(可以簡單理解為當某段代碼即將第一次被執(zhí)行時進行編譯,又稱即時編譯),通過對運行上下文的掃描,經(jīng)過逃逸分析,去除不可能存在共享資源競爭的鎖,通過這種方式消除沒有必要的鎖,可以節(jié)省毫無意義的請求鎖時間

          瀏覽 42
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  性爱在线网站 | 五月激情六月丁香 | 在线观看视频黄免费 | 成人娱乐中文字幕 | 一本色道久久88亚洲综合加勒比 |