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操作從工作記憶體中的一個變數的值傳送到主記憶體的變數中
如果要把一個變數從主記憶體中複製到工作記憶體中,就需要按順序地執行read和load操作, 如果把變數從工作記憶體中同步到主記憶體中,就需要按順序地執行store和write操作。但Java記憶體模型只要求上述操作必須按順序執行,而沒有保證必須是連續執行。
同步規則:
- 不允許一個執行緒無原因地(沒有發生過任何assign操作)把資料從工作記憶體同步回主記憶體 中
- 一個新的變數只能在主記憶體中誕生,不允許在工作記憶體中直接使用一個未被初始化(load 或者assign)的變數。即就是對一個變數實施use和store操作之前,必須先自行assign和load 操作。
- 一個變數在同一時刻只允許一條執行緒對其進行lock操作,但lock操作可以被同一執行緒重複 執行多次,多次執行lock後,只有執行相同次數的unlock操作,變數才會被解鎖。lock和 unlock必須成對出現。
- 如果對一個變數執行lock操作,將會清空工作記憶體中此變數的值,在執行引擎使用這個變 量之前需要重新執行load或assign操作初始化變數的值。
- 如果一個變數事先沒有被lock操作鎖定,則不允許對它執行unlock操作;也不允許去 unlock一個被其他執行緒鎖定的變數。
對一個變數執行unlock操作之前,必須先把此變數同步到主記憶體中(執行store和write操作)
Synchronized:
synchronized是jvm內建的同步鎖,它是隱式鎖,不需要我們自己手動釋放鎖。
每一個java物件中都有一個內部物件Monitor。synchronized就是通過內部物件Monitor(監視器鎖)實現,基於進入與退出Monitor物件實現方法與程式碼塊同步,監視器鎖的實現依賴底層作業系統的Mutex lock(互斥鎖)實現,它是一個重量級鎖效能較低(jdk1.6之後進行了優化)。
當我們在程式碼中使用了synchronized之後,可以在位元組碼檔案看到MONITORENTER和MONITOREXIT。Idea中安裝了ByteCode Viewer外掛就可以檢視位元組碼,選中編譯完的class檔案
java虛擬機器中ObjectMonitor的定義:(虛擬機器C++程式碼片段)
加鎖的過程:
Monitor.Enter和Monitor.Exit就是作用在JMM中定義的記憶體操作中的lock和unlock上面。然後從上面的同步規則中可以知道一個變數在同一時刻只允許一條執行緒對其進行lock操作,lock操作的時候會清空工作記憶體,重新去主記憶體load最新的資料。Unlock操作則會執行store和write操作將工作記憶體中的資料寫回主記憶體。這也就是為什麼我們用了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.Enter和Monitor.Exit。鎖的升級過程是不可逆的。
自旋鎖
虛擬機器為了避免執行緒真實地在作業系統層面掛起,會進行一項稱為自旋鎖的優化手段。它是一個過渡,每一次升級之前先進行自旋,比如通過一定的自旋之後發現還是偏向鎖鎖的場景那麼就不進行鎖的升級。這是基於在大多數情況下,執行緒持有鎖的時間都不會太長,如果直接掛起作業系統層面的執行緒可能會得不償失,畢竟作業系統實 現執行緒之間的切換時需要從使用者態轉換到核心態,這個狀態之間的轉換需要相對 比較長的時間,時間成本相對較高,因此自旋鎖會假設在不久將來,當前的執行緒 可以獲得鎖,因此虛擬機器會讓當前想要獲取鎖的執行緒做幾個空迴圈(這也是稱為 自旋的原因),一般不會太久,可能是50個迴圈或100迴圈,在經過若干次迴圈後,如果得到鎖,就順利進入臨界區。如果還不能獲得鎖,那就會將執行緒在操作 系統層面掛起,這就是自旋鎖的優化方式,這種方式確實也是可以提升效率的。
整個過程如下圖
&n