前面兩篇文章我介紹了一下html
看完你就會知道,線程若是鎖住了某個資源,導致其餘線程沒法訪問的這種鎖被稱爲悲觀鎖,相反,線程不鎖住資源的鎖被稱爲樂觀鎖,而自旋鎖是基於 CAS 機制實現的,CAS又是樂觀鎖的一種實現,那麼對於鎖來講,多個線程同步訪問某個資源的流程細節是否同樣呢?換句話說,在多線程同步訪問某個資源時,鎖的狀態會如何變化呢?本篇文章來探討一下。java
鎖狀態的分類git
Java 語言專門針對 synchronized
關鍵字設置了四種狀態,它們分別是:無鎖、偏向鎖、輕量級鎖和重量級鎖,可是在瞭解這些鎖以前還須要先了解一下 Java 對象頭和 Monitor。github
咱們知道 synchronized 是悲觀鎖,在操做同步以前須要給資源加鎖,這把鎖就是對象頭裏面的,而Java 對象頭又是什麼呢?咱們以 Hotspot 虛擬機爲例,Hopspot 對象頭主要包括兩部分數據:Mark Word(標記字段)
和 Klass Pointer(類型指針)
。安全
Mark Word:默認存儲對象的HashCode,分代年齡和鎖標誌位信息。這些信息都是與對象自身定義無關的數據,因此Mark Word被設計成一個非固定的數據結構以便在極小的空間內存存儲儘可能多的數據。它會根據對象的狀態複用本身的存儲空間,也就是說在運行期間Mark Word裏存儲的數據會隨着鎖標誌位的變化而變化。性能優化
Klass Point:對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是哪一個類的實例。網絡
在32位虛擬機和64位虛擬機的 Mark Word 所佔用的字節大小不同,32位虛擬機的 Mark Word 和 Klass Pointer 分別佔用 32bits 的字節,而 64位虛擬機的 Mark Word 和 Klass Pointer 佔用了64bits 的字節,下面咱們以 32位虛擬機爲例,來看一下其 Mark Word 的字節具體是如何分配的數據結構
用中文翻譯過來就是多線程
無鎖
的時候,對象頭開闢 25bit 的空間用來存儲對象的 hashcode ,4bit 用於存放分代年齡,1bit 用來存放是否偏向鎖的標識位,2bit 用來存放鎖標識位爲01偏向鎖
中劃分更細,仍是開闢25bit 的空間,其中23bit 用來存放線程ID,2bit 用來存放 epoch,4bit 存放分代年齡,1bit 存放是否偏向鎖標識, 0表示無鎖,1表示偏向鎖,鎖的標識位仍是01輕量級鎖
中直接開闢 30bit 的空間存放指向棧中鎖記錄的指針,2bit 存放鎖的標誌位,其標誌位爲00重量級鎖
中和輕量級鎖同樣,30bit 的空間用來存放指向重量級鎖的指針,2bit 存放鎖的標識位,爲11GC標記
開闢30bit 的內存空間卻沒有佔用,2bit 空間存放鎖標誌位爲11。其中無鎖和偏向鎖的鎖標誌位都是01,只是在前面的1bit區分了這是無鎖狀態仍是偏向鎖狀態。oracle
關於爲何這麼分配的內存,咱們能夠從 OpenJDK
中的 markOop.hpp 類中的枚舉窺出端倪
來解釋一下
synchronized
用的鎖是存在Java對象頭裏的。
JVM基於進入和退出 Monitor 對象來實現方法同步和代碼塊同步。代碼塊同步是使用 monitorenter 和 monitorexit 指令實現的,monitorenter 指令是在編譯後插入到同步代碼塊的開始位置,而 monitorexit 是插入到方法結束處和異常處。任何對象都有一個 monitor 與之關聯,當且一個 monitor 被持有後,它將處於鎖定狀態。
根據虛擬機規範的要求,在執行 monitorenter 指令時,首先要去嘗試獲取對象的鎖,若是這個對象沒被鎖定,或者當前線程已經擁有了那個對象的鎖,把鎖的計數器加1,相應地,在執行 monitorexit 指令時會將鎖計數器減1,當計數器被減到0時,鎖就釋放了。若是獲取對象鎖失敗了,那當前線程就要阻塞等待,直到對象鎖被另外一個線程釋放爲止。
Synchronized是經過對象內部的一個叫作監視器鎖(monitor)來實現的,監視器鎖本質又是依賴於底層的操做系統的 Mutex Lock(互斥鎖)來實現的。而操做系統實現線程之間的切換須要從用戶態轉換到核心態,這個成本很是高,狀態之間的轉換須要相對比較長的時間,這就是爲何 Synchronized 效率低的緣由。所以,這種依賴於操做系統 Mutex Lock 所實現的鎖咱們稱之爲重量級鎖
。
Java SE 1.6爲了減小得到鎖和釋放鎖帶來的性能消耗,引入了偏向鎖
和輕量級鎖
:鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態。鎖能夠升級但不能降級。
因此鎖的狀態總共有四種:無鎖狀態、偏向鎖、輕量級鎖和重量級鎖。隨着鎖的競爭,鎖能夠從偏向鎖升級到輕量級鎖,再升級的重量級鎖(可是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級)。JDK 1.6中默認是開啓偏向鎖和輕量級鎖的,咱們也能夠經過-XX:-UseBiasedLocking=false來禁用偏向鎖。
無鎖狀態
,無鎖即沒有對資源進行鎖定,全部的線程均可以對同一個資源進行訪問,可是隻有一個線程可以成功修改資源。
無鎖的特色就是在循環內進行修改操做,線程會不斷的嘗試修改共享資源,直到可以成功修改資源並退出,在此過程當中沒有出現衝突的發生,這很像咱們在以前文章中介紹的 CAS 實現,CAS 的原理和應用就是無鎖的實現。無鎖沒法全面代替有鎖,但無鎖在某些場合下的性能是很是高的。
Hotspot 的做者通過研究發現,大多數狀況下,鎖不只不存在多線程競爭,還存在鎖由同一線程屢次得到的狀況,偏向鎖就是在這種狀況下出現的,它的出現是爲了解決只有在一個線程執行同步時提升性能。
能夠從對象頭的分配中看到,偏向鎖要比無鎖多了線程ID
和 epoch
,當一個線程訪問同步代碼塊並獲取鎖時,會在對象頭和棧幀的記錄中存儲線程的ID,等到下一次線程在進入和退出同步代碼塊時就不須要進行 CAS
操做進行加鎖和解鎖,只須要簡單判斷一下對象頭的 Mark Word 中是否存儲着指向當前線程的線程ID,判斷的標誌固然是根據鎖的標誌位來判斷的。
訪問 Mark Word 中偏向鎖的標誌是否設置成 1,鎖的標誌位是不是 01 --- 確認爲可偏向狀態。
若是確認爲可偏向狀態,判斷當前線程id 和 對象頭中存儲的線程 ID 是否一致,若是一致的話,則執行步驟5,若是不一致,進入步驟3
若是當前線程ID 與對象頭中存儲的線程ID 不一致的話,則經過 CAS 操做來競爭獲取鎖。若是競爭成功,則將 Mark Word 中的線程ID 修改成當前線程ID,而後執行步驟5,若是不一致,則執行步驟4
若是 CAS 獲取偏向鎖失敗,則表示有競爭(CAS 獲取偏向鎖失敗則代表至少有其餘線程曾經獲取過偏向鎖,由於線程不會主動釋放偏向鎖)。當到達全局安全點(SafePoint)時,會首先暫停擁有偏向鎖的線程,而後檢查持有偏向鎖的線程是否存活(由於可能持有偏向鎖的線程已經執行完畢,可是該線程並不會主動去釋放偏向鎖),若是線程不處於活動狀態,則將對象頭置爲無鎖狀態(標誌位爲01)
,而後從新偏向新的線程;若是線程仍然活着,撤銷偏向鎖後升級到輕量級鎖
的狀態(標誌位爲00
),此時輕量級鎖由原持有偏向鎖的線程持有,繼續執行其同步代碼,而正在競爭的線程會進入自旋等待得到該輕量級鎖。
執行同步代碼
偏向鎖的釋放過程能夠參考上述的步驟4 ,偏向鎖在遇到其餘線程競爭鎖時,持有偏向鎖的線程纔會釋放鎖,線程不會主動釋放偏向鎖。偏向鎖的撤銷,須要等待全局安全點(在這個時間點上沒有字節碼正在執行),它會首先暫停擁有偏向鎖的線程,判斷鎖是否處於被鎖定狀態,撤銷偏向鎖後恢復到未鎖定(標誌位爲01
)或輕量級鎖(標誌位爲00
)的狀態。
偏向鎖在Java 6 和Java 7 裏是默認啓用的。因爲偏向鎖是爲了在只有一個線程執行同步塊時提升性能,若是你肯定應用程序裏全部的鎖一般狀況下處於競爭狀態,能夠經過JVM參數關閉偏向鎖:-XX:-UseBiasedLocking=false,那麼程序默認會進入輕量級鎖狀態。
真正理解 epoch 的概念比較複雜,這裏簡單理解,就是 epoch 的值能夠做爲一種檢測偏向鎖有效性的時間戳
輕量級鎖
是指當前鎖是偏向鎖的時候,被另外的線程所訪問,那麼偏向鎖就會升級爲輕量級鎖
,其餘線程會經過自旋的形式嘗試獲取鎖,不會阻塞,從而提升性能。
在代碼進入同步塊的時候,若是同步對象鎖狀態爲無鎖狀態(鎖標誌位爲 01 狀態,是否爲偏向鎖爲 0 ),虛擬機首先將在當前線程的棧幀中創建一個名爲鎖記錄(Lock Record)
的空間,用於存儲鎖對象目前的 Mark Word 的拷貝,而後拷貝對象頭中的 Mark Word 複製到鎖記錄中。
拷貝成功後,虛擬機將使用 CAS 操做嘗試將對象的 Mark Word 更新爲指向 Lock Record 的指針,並將 Lock Record裏的 owner 指針指向對象的 Mark Word。
若是這個更新動做成功了,那麼這個線程就擁有了該對象的鎖,而且對象Mark Word的鎖標誌位設置爲 00 ,表示此對象處於輕量級鎖定狀態。
若是這個更新操做失敗了,虛擬機首先會檢查對象的 Mark Word 是否指向當前線程的棧幀,若是是就說明當前線程已經擁有了這個對象的鎖,那就能夠直接進入同步塊繼續執行。不然說明多個線程競爭鎖,輕量級鎖就要膨脹爲重量級鎖,鎖標誌的狀態值變爲 10 ,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,後面等待鎖的線程也要進入阻塞狀態。
重量級鎖也就是一般說 synchronized 的對象鎖,鎖標識位爲10,其中指針指向的是 monitor 對象(也稱爲管程或監視器鎖)的起始地址。每一個對象都存在着一個 monitor 與之關聯,對象與其 monitor 之間的關係有存在多種實現方式,如 monitor 能夠與對象一塊兒建立銷燬或當線程試圖獲取對象鎖時自動生成,但當一個 monitor 被某個線程持有後,它便處於鎖定狀態。
上圖簡單描述多線程獲取鎖的過程,當多個線程同時訪問一段同步代碼時,首先會進入 Entry Set當線程獲取到對象的 monitor 後進入 The Owner 區域並把 monitor 中的 owner 變量設置爲當前線程,同時 monitor 中的計數器count 加1,若線程調用 wait() 方法,將釋放當前持有的 monitor,owner變量恢復爲 null,count自減1,同時該線程進入 WaitSet 集合中等待被喚醒。若當前線程執行完畢也將釋放 monitor (鎖)並復位變量的值,以便其餘線程進入獲取monitor(鎖)。
由此看來,monitor 對象存在於每一個Java對象的對象頭中(存儲的指針的指向),synchronized 鎖即是經過這種方式獲取鎖的,也是爲何Java中任意對象能夠做爲鎖的緣由,同時也是 notify/notifyAll/wait 等方法存在於頂級對象Object中的緣由。(部分來源於網絡)
下面爲本身作個宣傳,歡迎關注公衆號 Java建設者,號主是Java技術棧,熱愛技術,喜歡閱讀,熱衷於分享和總結,但願能把每一篇好文章分享給成長道路上的你。關注公衆號回覆 002 領取爲你特地準備的大禮包,你必定會喜歡並收藏的。
文章參考:
Synchronized鎖性能優化偏向鎖輕量級鎖升級 多線程中篇(五)
citeseerx.ist.psu.edu/viewdoc/dow…
[java 偏向鎖、輕量級鎖及重量級鎖synchronized原理](