中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

java中synchronized鎖升級的示例分析

發布時間:2021-09-13 12:57:35 來源:億速云 閱讀:124 作者:小新 欄目:開發技術

小編給大家分享一下java中synchronized鎖升級的示例分析,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!

synchronized鎖的升級(偏向鎖、輕量級鎖及重量級鎖)

java同步鎖前置知識點

  • 1.編碼中如果使用鎖可以使用synchronized關鍵字,對方法、代碼塊進行同步加鎖

  • 2.Synchronized同步鎖是jvm內置的隱式鎖(相對Lock,隱式加鎖與釋放)

  • 3.Synchronized同步鎖的實現依賴于操作系統,獲取鎖與釋放鎖進行系統調用,會引起用戶態與內核態切換

  • 4.jdk1.5之前加鎖只能使用synchronized,1.6引入Lock同步鎖(請求鎖基于java實現,顯式加鎖與釋放、性能更優)

  • 5.jdk1.6對于Synchronzied同步鎖提出了偏向鎖、輕量級鎖、重量級鎖的概念(其實是對synchronized的性能優化,盡可能減少鎖競爭帶來的上下文切換)

  • 6.無論是使用synchronized還是Lock,線程上下文切換都是無法避免的

  • 7.Lock相對synchronized的性能優化的其中一點是:在線程阻塞的時候,Lock獲取鎖不會導致用戶態與內核態的切換,而synchronized會(看第3點)。但是線程阻塞都會導致上下文切換(看第6點)

  • 8.java線程的阻塞與喚醒依賴操作系統調用,導致用戶態與內核態切換

  • 9.前面說的用戶態與內核態切換發生的是進程上下文切換而非線程上下文切換

本文主要關注synchronized鎖的升級。

synchronized同步鎖

java對象頭

每個java對象都有一個對象頭,對象頭由類型指針和標記字段組成。

在64位虛擬機中,未開啟壓縮指針,標記字段占64位,類型指針占64位,共計16個字節。

鎖類型信息為標記字段的最后2位:00表示輕量級鎖,01表示無鎖或偏向鎖,10表示重量級鎖;如果倒數第3位為1表示這個類的偏向鎖啟用,為0表示類的偏向鎖被禁用。

如下圖,圖片來源wiki

java中synchronized鎖升級的示例分析

左側一列表示偏向鎖啟用(方框1),右側一列表示偏向鎖禁用(方框3)。1和3都表示無鎖的初始狀態,如果啟用偏向鎖,鎖升級的步驟應該是1->2->4->5,如果禁用偏向鎖,鎖升級步驟是3->4->5。

我用的jdk8,打印了參數看了下,默認是啟用偏向鎖,如要是禁用: -XX:-UseBiasedLocking

java中synchronized鎖升級的示例分析

關于偏向鎖還有另外幾個參數:

java中synchronized鎖升級的示例分析

注意BiasedLockingStartupDelay參數,默認值4000ms,表示虛擬機啟動的延遲4s才會使用偏向鎖(先使用輕量級鎖)。

偏向鎖

偏向鎖處理的場景是大部分時間只有同一條線程在請求鎖,沒有多線程競爭鎖的情況。看對象頭圖的紅框2,有個thread ID字段:當第一次線程加鎖的時候,jvm通過cas將當前線程地址設置到thread ID標記位,最后3位是101。下次同一線程再獲取鎖的時候只用檢查最后3位是否為101,是否為當前線程,epoch是否和鎖對象的類的epoch相等(wiki上說沒有再次cas設置是為了針對現在多處理器上的cas操作的優化)。

偏向鎖優化帶來的性能提升指的是避免了獲取鎖進行系統調用導致的用戶態和內核態的切換,因為都是同一條線程獲取鎖,沒有必要每次獲取鎖的時候都要進行系統調用。

如果當前線程獲取鎖的時候(無鎖狀態下)線程ID與當前線程不匹配,會將偏向鎖撤銷,重新偏向當前線程,如果次數達到BiasedLockingBulkRebiasThreshold的值,默認20次,當前類的偏向鎖失效,影響就是epoch的值變動,加鎖類的epoch值加1,后續鎖對象會重新copy類的epoch值到圖中的epoch標記位。如果總撤銷次數達到BiasedLockingBulkRevokeThreshold的值(默認40次),就禁用當前類的偏向鎖了,就是對象頭右側列了,加鎖直接從輕量鎖開始了(鎖升級了)。

偏向鎖的撤銷是個很麻煩的過程,需要所有線程達到安全點(發生STW),遍歷所有線程的線程棧檢查是否持有鎖對象,避免丟鎖,還有就是對epoch的處理。

如果存在多線程競爭,那偏向鎖就要升級了,升級到輕量級鎖。

輕量級鎖

輕量級鎖處理的場景是在同的時間段有不同的線程請求鎖(線程交替執行)。即使同一時間段,存在多條線程競爭鎖,獲取到鎖的線程持有鎖的時間也特別短,很快就釋放鎖了。

線程加鎖的時候,判斷不是重量級鎖,就會在當前線程棧內開辟一個空間,作為鎖記錄,將鎖對象頭的標記字段復制過來(復制過來是做一個記錄,因為后面要把鎖對象頭的標記字段的值替換為剛才復制這個標記字段的空間地址,就像對象頭那個圖片中的pointer to lock record部分,至于最后2位,因為是內存對齊的緣故,所以是00)。然后基于CAS操作將復制這個標記字段的地址設置為鎖對象頭的標記位的值,如果成功就是獲取到鎖了。如果加鎖的時候判斷不是重量級鎖,最后兩位也不是01(從偏向鎖或無鎖狀態過來的),那就說明已經有線程持有了,如果是當前線程在(需要重入),那就設置一個0,這里是個棧結構,直接壓入一個0即可。最后釋放鎖的時候,出棧,最后一個元素記錄的就是鎖對象原來的標記字段的值,再通過CAS設置到鎖對象頭即可。

注意在獲取鎖的時候,cas失敗,當前線程會自旋一會,達到一定次數,升級到重量級鎖,當前線程也會阻塞。

重量級鎖

重量級就是我們平常說的加的同步鎖,也就是java基礎的鎖實現,獲取鎖與釋放鎖的時候都要進行系統調用,從而導致上下文切換。

關于自旋鎖

關于自旋鎖,我查閱相關資料,主要有兩種說明:

1、是輕量級鎖競爭失敗,不會立即膨脹為重量級而是先自旋一定次數嘗試獲取鎖;

2、是重量級鎖競爭失敗也不會立即阻塞,也是自旋一定次數(這里涉及到一個自調整算法)。

關于這個說明,還是要看jvm的源碼實現才能確定哪個是真實的:

打印偏向鎖的參數

如下:

-XX:+UnlockDiagnosticVMOptions

-XX:+PrintBiasedLockingStatistics

我在main方法循環獲取同一把鎖,打印結果如下:

public static void main(String[] args) {
        int num = 0;
        for (int i = 0; i < 1_000_000000; i++) {
            synchronized (lock) {
                num++;
            }
        }
    }

java中synchronized鎖升級的示例分析

synchronized原理解析

一:synchronized原理解析

1:對象頭

首先,我們要知道對象在內存中的布局:

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

  • 對象頭zhuyao是由MarkWord和Klass Point(類型指針)組成,其中Klass Point是對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例,Mark Word用于存儲對象自身的運行時數據。如果對象是數組對象,那么對象頭占用3個字寬(Word),如果對象是非數組對象,那么對象頭占用2個字寬。(1word = 2 Byte = 16 bit)。

  • 實例變量存儲的是對象的屬性信息,包括父類的屬性信息,按照4字節對齊。

  • 填充字符,因為虛擬機要求對象字節必須是8字節的整數倍,填充字符就是用于湊齊這個整數倍的。

java中synchronized鎖升級的示例分析

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

在32位的虛擬機中:

java中synchronized鎖升級的示例分析

在64位的虛擬機中:

java中synchronized鎖升級的示例分析

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

2:Synchronized在JVM中的實現原理

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

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

光看這些數據結構對監視器鎖的工作機制還是一頭霧水,那么我們首先看一下線程在獲取鎖的幾個狀態的轉換:

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

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

  • 當多個線程同時訪問該方法,那么這些線程會先被放進_EntryList隊列,此時線程處于blocking狀態。

  • 當一個線程獲取到了實例對象的監視器(monitor)鎖,那么就可以進入running狀態,執行方法,此時,ObjectMonitor對象的_owner指向當前線程,_count加1表示當前對象鎖被一個線程獲取

  • 當running狀態的線程調用wait()方法,那么當前線程釋放monitor對象,進入waiting狀態,ObjectMonitor對象的_owner變為null,_count減1,同時線程進入_WaitSet隊列,直到有線程調用notify()方法喚醒該線程,則該線程重新獲取monitor對象進入_Owner區

  • 如果當前線程執行完畢,那么也釋放monitor對象,進入waiting狀態,ObjectMonitor對象的_owner變為null,_count減1

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

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

(1)Synchronized修飾代碼塊:

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

例如,同步代碼塊如下:

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

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

  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
        24: return
      Exception table:
      //省略其他字節碼.......

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

(2)Synchronized修飾方法:

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

同步方法代碼如下:

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

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

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
        10: return
      LineNumberTable:
        line 12: 0
        line 13: 10
}

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

三、鎖的優化

1、鎖升級

鎖的4中狀態:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態(級別從低到高)

(1)偏向鎖:

為什么要引入偏向鎖?

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

偏向鎖的升級:

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

偏向鎖的取消:

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

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

(2)輕量級鎖

為什么要引入輕量級鎖?

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

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

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

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

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

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

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

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

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

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

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

以上是“java中synchronized鎖升級的示例分析”這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業資訊頻道!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

洛宁县| 湘潭市| 邛崃市| 平果县| 蓝山县| 甘谷县| 海南省| 清流县| 招远市| 新巴尔虎右旗| 垦利县| 龙泉市| 台东县| 镇坪县| 大新县| 繁峙县| 南溪县| 柳河县| 苍溪县| 垣曲县| 石渠县| 台南市| 鱼台县| 长汀县| 五莲县| 阜宁县| 耒阳市| 龙州县| 南宁市| 济南市| 垦利县| 射阳县| 新河县| 鄢陵县| 宁明县| 靖边县| 祁东县| 沽源县| 曲靖市| 龙南县| 武穴市|