Synchronized加鎖、鎖升級和java對象內存結構

首先了解一下JMM中定義的內存操作:

一個線程操作數據時候都是從主內存(堆內存)讀取到自己工作內存(線程私有的數據區域)中再進行操作。對於硬件內存來說,並沒有工作內存和主內存的區分,這都是java內存模型劃分出來的,它只是一種抽象的概念,是一組規則,並不是實際存在的。Java內存模型中定義了八種同步操作

1.lock(鎖定):作用於主內存的變量,把一個變量標記爲一條線程獨佔狀態

2.unlock(解鎖):作用於主內存的變量,把一個處於鎖定狀態的變量釋放出來,釋放後的變量纔可以被其他線程鎖定

3.read(讀取):作用於主內存的變量,把一個變量值從主內存傳輸到線程的工作內存中,以便隨後的load動作使用

4.load(載入):作用於工作內存的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中

5.use(使用):作用於工作內存的變量,把工作內存中的一個變量值傳遞給執行引擎

6.assign(賦值):作用於工作內存的變量,它把一個從執行引擎接收到的值賦給工作內存的變量

7.store(存儲):作用於工作內存的變量,把工作內存中的一個變量的值傳送到主內存中,以便隨後的write的操作

8.write(寫入):作用於工作內存的變量,它把store操作從工作內存中的一個變量的值傳送到主內存的變量中

如果要把一個變量從主內存中複製到工作內存中,就需要按順序地執行readload操作, 如果把變量從工作內存中同步到主內存中,就需要按順序地執行storewrite操作。但Java內存模型只要求上述操作必須按順序執行,而沒有保證必須是連續執行。

同步規則:

  1. 不允許一個線程無原因地(沒有發生過任何assign操作)把數據從工作內存同步回主內存 中
  2. 一個新的變量只能在主內存中誕生,不允許在工作內存中直接使用一個未被初始化(load 或者assign)的變量。即就是對一個變量實施usestore操作之前,必須先自行assignload 操作。
  3. 一個變量在同一時刻只允許一條線程對其進行lock操作,但lock操作可以被同一線程重複 執行多次,多次執行lock後,只有執行相同次數的unlock操作,變量纔會被解鎖。lockunlock必須成對出現。
  4. 如果對一個變量執行lock操作,將會清空工作內存中此變量的值,在執行引擎使用這個變 量之前需要重新執行loadassign操作初始化變量的值。
  5. 如果一個變量事先沒有被lock操作鎖定,則不允許對它執行unlock操作;也不允許去 unlock一個被其他線程鎖定的變量。

對一個變量執行unlock操作之前,必須先把此變量同步到主內存中(執行storewrite操作)

 

Synchronized

synchronizedjvm內置的同步鎖,它是隱式鎖,不需要我們自己手動釋放鎖。

每一個java對象中都有一個內部對象Monitorsynchronized就是通過內部對象Monitor(監視器鎖)實現,基於進入與退出Monitor對象實現方法與代碼塊同步,監視器鎖的實現依賴底層操作系統的Mutex lock(互斥鎖)實現,它是一個重量級鎖性能較低jdk1.6之後進行了優化)。

當我們在代碼中使用了synchronized之後,可以在字節碼文件看到MONITORENTERMONITOREXITIdea中安裝了ByteCode Viewer插件就可以查看字節碼,選中編譯完的class文件

 

 

 

 

java虛擬機中ObjectMonitor的定義:(虛擬機C++代碼片段)

 

 

加鎖的過程:

 

 

 

 Monitor.EnterMonitor.Exit就是作用在JMM中定義的內存操作中的lockunlock上面。然後從上面的同步規則中可以知道一個變量在同一時刻只允許一條線程對其進行lock操作lock操作的時候會清空工作內存,重新去主內存load最新的數據。Unlock操作則會執行storewrite操作將工作內存中的數據寫回主內存。這也就是爲什麼我們用了Synchronized關鍵字之後就能夠實現線程安全。

 

Java對象內存結構:

對象在內存中存儲的結構由三部分組成:對象頭主要是一些標記信息MarkWord,比如hashcode,鎖狀態這些;實例數據就是真實的數據;對齊填充要求對象大小8字節的整數倍,如果不是就填充補齊。

 

 MarkWord鎖狀態標記就在這裏面,以32位jvm爲例,64位也是這些東西,只是佔的大小不一樣

 

無鎖狀態:前25位記錄的是hashcode,後四位是對象分代年齡,然後是否是偏向鎖標記

偏向鎖狀態:前23位是偏向的線程ID

輕量級鎖:前30位指向線程棧中鎖記錄的指針

重量級鎖:前30位指向重量級鎖Monitor的指針

 

 

JVM內置鎖優化升級

JDK1.6版本之後對synchronized的實現進行了各種優化,自旋鎖、偏向鎖和輕量級鎖

並默認開啓偏向鎖

開啓偏向鎖:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0

關閉偏向鎖:-XX:-UseBiasedLocking

偏向鎖

  偏向鎖是Java 6之後加入的新鎖,它是一種針對加鎖操作的優化手段,經過研究發現,在大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,因此爲了減少同一線程獲取鎖(會涉及到一些CAS操作,耗時)的代價而引入偏向鎖。偏向鎖的核心思想是,如果一個線程獲得了鎖,那麼鎖就進入偏向模式,此時Mark Word 的結構也變爲偏向鎖結構,當這個線程再次請求鎖時,無需 再做任何同步操作,即獲取鎖的過程,這樣就省去了大量有關鎖申請的操作,從 而也就提供程序的性能。所以,對於沒有鎖競爭的場合,偏向鎖有很好的優化效 果,畢竟極有可能連續多次是同一個線程申請相同的鎖。但是對於鎖競爭比較激 烈的場合,偏向鎖就失效了,因爲這樣場合極有可能每次申請鎖的線程都是不相 同的,因此這種場合下不應該使用偏向鎖,否則會得不償失,需要注意的是,偏向鎖失敗後,並不會立即膨脹爲重量級鎖,而是先升級爲輕量級鎖。

輕量級鎖

  倘若偏向鎖失敗,虛擬機並不會立即升級爲重量級鎖,它還會嘗試使用一種 稱爲輕量級鎖的優化手段(1.6之後加入的),此時Mark Word 的結構也變爲輕量 級鎖的結構。輕量級鎖能夠提升程序性能的依據是“對絕大部分的鎖,在整個同 步週期內都不存在競爭”,注意這是經驗數據。需要了解的是,輕量級鎖所適應 的場景是線程交替執行同步塊的場合,如果存在同一時間訪問同一鎖的場合,就會導致輕量級鎖膨脹爲重量級鎖。這個時候也就是上面的Monitor.EnterMonitor.Exit鎖的升級過程是不可逆的。

自旋鎖

  虛擬機爲了避免線程真實地在操作系統層面掛起,會進行一項稱爲自旋鎖的優化手段。它是一個過渡,每一次升級之前先進行自旋,比如通過一定的自旋之後發現還是偏向鎖鎖的場景那麼就不進行鎖的升級。這是基於在大多數情況下,線程持有鎖的時間都不會太長,如果直接掛起操作系統層面的線程可能會得不償失,畢竟操作系統實 現線程之間的切換時需要從用戶態轉換到核心態,這個狀態之間的轉換需要相對 比較長的時間,時間成本相對較高,因此自旋鎖會假設在不久將來,當前的線程 可以獲得鎖,因此虛擬機會讓當前想要獲取鎖的線程做幾個空循環(這也是稱爲 自旋的原因),一般不會太久,可能是50個循環或100循環,在經過若干次循環後,如果得到鎖,就順利進入臨界區。如果還不能獲得鎖,那就會將線程在操作 系統層面掛起,這就是自旋鎖的優化方式,這種方式確實也是可以提升效率的。

整個過程如下圖

 

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