深入瞭解Synchronized原理

JieMingLi發表於2019-05-04

Synchronized

互斥性

同一個時間只允許一個執行緒擁有一個物件鎖,這樣在同一時間只有一個執行緒對需要同步的程式碼塊進行訪問

可見性

必須確保在某個執行緒的某個物件鎖在釋放之前,對某個共享變數所做的改變,對於下一個擁有在這個物件鎖的執行緒是可見的,否則另外執行緒讀取的是本地的副本從而進行操作,導致結果不一致。

重入性

從互斥鎖的設計上來說,一個執行緒試圖操作一個由其他執行緒持有的臨界資源的時候,這個執行緒會處於堵塞狀態。

如果一個執行緒再次請求自己持有物件鎖的臨界資源的時候,這就屬於重入鎖。

因此在一個執行緒呼叫synchronized方法的同時在其方法體內部呼叫該物件另一個synchronized方法,也就是說一個執行緒得到一個物件鎖後再次請求該物件鎖,是允許的,這就是synchronized的可重入性。

獲取物件鎖的方式

獲取物件鎖的方式

  1. 修飾例項方法,作用於當前例項加鎖,進行同步程式碼塊之前需要獲得當前例項的鎖(Synchronized method)
  2. 修飾程式碼塊,指定加鎖物件,作用於給定物件加鎖,進入同步程式碼快之前要獲得給定物件的鎖(Synchronized instance)
  3. 修飾靜態方法,作用於當前類物件加鎖,進入同步程式碼塊之前要獲得當前類物件的鎖(Synchronized static method)
  4. 修飾類物件,作用於類物件加鎖,進入同步程式碼塊之前要獲得指定類物件的鎖(Synchronized **.class)

物件鎖和類鎖的區別

  1. 一個執行緒可以訪問物件的同步程式碼塊時,另外一個執行緒也可以訪問同一個物件的非同步程式碼塊
  2. 若鎖住的是同一個物件,其他執行緒訪問物件的同步程式碼塊或者同步方法的時候會被阻塞
  3. 同一個類的不同物件的物件鎖互不干擾
  4. 類鎖是一種特殊的鎖,因為類就是Class的例項,所以只要不同物件都是屬於同一個類,那麼他們的類鎖都是一樣的
  5. 類鎖和物件鎖互不干擾

底層原理

深入瞭解Synchronized原理

鎖物件儲存在Java物件頭裡面

位數 頭物件結構
32 Mark word 儲存物件的HashCode,GC分代年齡,鎖型別,鎖標記
32 Class MeteDataAddress 型別指標:指向例項物件所屬的類

MarkWord被設定為一個非固定的資料結構,用來儲存更多的資料,結構如下(這裡不是很懂)

深入瞭解Synchronized原理

Monitor(內部鎖,Monitor鎖,管程,監視器鎖,也就是和物件鎖對應的物件)

每個物件都存在這一個Monitor與之關聯

每個Java物件天生帶有這把看不見的鎖,在MarkWord的結構中,重量級鎖的標記為是10,也就是指標就是指向Monitor物件的起始地址,在這裡也就說明了Synchronized的預設鎖是重量級鎖。monitor可以與物件一起建立銷燬或當執行緒試圖獲取物件鎖時自動生成,但當一個 monitor 被某個執行緒持有後,它便處於鎖定狀態

在Java虛擬機器中,Monitor是有MonitorObject所實現的,部分結構如下

深入瞭解Synchronized原理

_owner:指向持有ObjectMonitor物件的執行緒

_WaitSet:存放處於wait狀態的執行緒佇列

_EntryList:存放處於等待鎖block狀態的執行緒佇列

_count:用來記錄該執行緒獲取鎖的次數

ObjectMonitor中有兩個佇列,_WaitSet 和 _EntryList,用來儲存ObjectWaiter物件列表( 每個等待鎖的執行緒都會被封裝成ObjectWaiter物件),_owner指向持有ObjectMonitor物件的執行緒,當有多個執行緒訪問同一塊同步程式碼塊的時候,執行緒會執行緒會進入_EntryList,當執行緒獲取到物件的monitor 後進入 _Owner 區域並把monitor中的owner變數設定為當前執行緒,同時monitor中的計數器count加1,若執行緒呼叫 wait() 方法,將釋放當前持有的monitor,owner變數恢復為null,count自減1,同時該執行緒進入 WaitSet集合中等待被喚醒。若當前執行緒執行完畢也將釋放monitor(鎖)並復位變數的值,以便其他執行緒進入獲取monitor(鎖)。

Monitorenter和Monitorexit

Synchronized程式碼塊執行原理

位元組碼中可知同步語句塊的實現使用的是monitorentermonitorexit 指令,其中monitorenter指令指向同步程式碼塊的開始位置,monitorexit指令則指明同步程式碼塊的結束位置 。當執行monitorenter指令時,如果當前執行緒獲取物件鎖所對應的monitor的特權的時候

1 會去檢查monitor的物件的count是否為0

2 如果為0的話就獲取成功,並且將count置為1

3 倘若其他執行緒已經擁有 objectref 的 monitor 的所有權,那當前執行緒將被阻塞,直到正在執行執行緒執行完畢,即monitorexit指令被執行,執行執行緒將釋放 monitor(鎖)並設定計數器值為0 ,其他執行緒將有機會持有 monitor 。

編譯器將會確保無論方法通過何種方式完成,方法中呼叫過的每條 monitorenter 指令都有執行其對應 monitorexit 指令,而無論這個方法是正常結束還是異常結束。為了保證在方法異常完成時 monitorenter 和 monitorexit 指令依然可以正確配對執行,編譯器會自動產生一個異常處理器,這個異常處理器宣告可處理所有的異常,它的目的就是用來執行 monitorexit 指令。一般位元組碼檔案中都會多出一條monitorexit指令。

Synchronized方法執行原理

方法級的同步是隱式,即無需通過位元組碼指令來控制的,它實現在方法呼叫和返回操作之中。JVM可以從ACC_SYNCHRONIZED 訪問標誌區分一個方法是否同步方法。當方法呼叫時,呼叫指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設定,如果設定了,執行執行緒將先持有monitor,然後再執行方法,最後再方法完成(無論是正常完成還是非正常完成)時釋放monitor。

如果一個同步方法執行期間丟擲了異常,並且在方法內部無法處理此異常,那這個同步方法所持有的monitor將在異常拋到同步方法之外時自動釋放

鎖的型別

自旋鎖

synchronized在jdk1.6之前的鎖是重量級鎖,對於互斥同步的效能來說,阻塞掛起的是影響最大的。因為掛起執行緒和恢復執行緒都是要讓作業系統從使用者態轉化到核心態中完成,而這兩個狀態的轉換是比較影響效能的。

大多數情況下,執行緒擁有鎖的時間不會太長,如果直接掛起的話,會影響系統的效能。因為前面說過,執行緒切換是需要在作業系統的使用者態和核心態之間轉換的。所以為了解決這個問題,引進了自旋鎖。

自旋鎖假設在不久,當前執行緒可以獲得這個鎖,因此JVM就讓這個想要獲得鎖的執行緒,先做幾個空迴圈先,讓這個執行緒先不要放棄佔有CPU資源的機會,經過若干次空迴圈之後,如果獲得鎖,那麼就順利的進入臨界區。否則,你也不能讓這個執行緒一直佔有CPU資源呀,所以經過大概10次空迴圈之後,就只能老老實實地掛起了。

自旋適應鎖

自旋適應鎖就是從自旋鎖改進而來的。在自旋鎖的基礎上,假如A執行緒通過自旋一定的時間之後獲得了鎖,然後釋放鎖。這時B執行緒也獲得了這個鎖,如果此時A執行緒再次想得到這個鎖,那麼JVM就會根據之前A執行緒曾經獲得過這個鎖,那麼我就給你適當地增加一點空迴圈的次數,比如說從10次空迴圈到100次。假如有個C執行緒,他也想獲得這個鎖,也得自旋等待,可是很少輪到他或者沒得到過這個鎖(可能是被A搶了機會或者其他的),那麼JVM就會認為C執行緒以後可能沒什麼機會獲得了,就適當地減少C執行緒的空循壞次數甚至不讓他做空迴圈。

偏向鎖

如果A執行緒第一次獲得鎖,那麼鎖就進入偏向模式(虛擬機器把物件頭中的標誌位設為“01”),MarkWord的結構也變成偏向鎖結構,如果沒有其他執行緒和A執行緒競爭,A執行緒再次請求該鎖時,無需任何同步操作

只需要檢查MarkWord的鎖標記位是否為偏向鎖和當前執行緒的Id是否為ThreadId即可。

也就是說當一個執行緒訪問同步塊並且獲取鎖的時候,會通過CAS操作在物件頭的偏向鎖結構裡記錄執行緒的ID,如果記錄成功,執行緒在進入和退出同步塊時,不需要進行CAS操作來加鎖和解鎖,從而提高程式的效能。

TIPS:偏向鎖只能被第一個獲取它的執行緒進行 CAS 操作,一旦出現執行緒競爭鎖物件,其它執行緒無論何時進行 CAS 操作都會失敗。

加鎖具體步驟如下

  1. 先檢查Mark Word是否為可偏向狀態,也就是說是否 是偏向鎖1,鎖標識位為01

  2. 如果是可偏向狀態,那麼就測試Mark Word結構的執行緒ID是不是和當前執行緒的ID一致,

    如果是就直接執行同步程式碼塊。

    如果不是就通過CAS操作競爭鎖,

    ​ 如果操作成功,就把Mark Word的執行緒ID設定為執行緒的ID

    ​ 如果操作失敗,那麼就說明此時有多執行緒競爭的狀態,等到安全點,獲得偏向鎖的執行緒就掛起,進行解鎖操作。偏向鎖升級為輕量鎖,被阻塞在安全點的執行緒繼續往下執行同步程式碼塊。

解鎖

當獲得偏向鎖的執行緒掛起之後,就會進行解鎖操作。

在解鎖成功之後,JVM判斷此時執行緒的狀態,

如果還沒有執行完同步程式碼,則直接將偏向鎖升級為輕量級鎖,然後繼續執行剩下的程式碼塊。

如果此時已經執行完同步程式碼,則撤銷鎖為無鎖狀態,以後執行同步程式碼的時候JVM則會直接升級為輕量鎖。

深入瞭解Synchronized原理

輕量鎖(加鎖解鎖操作是需要依賴多次CAS原子指令的)

偏向鎖一旦受到多執行緒競爭,就會膨脹為輕量鎖

獲取鎖

  1. 先判斷當前物件是否處於無鎖狀態,如果是,JVM就首先在想要獲取這個鎖的執行緒的棧幀中建立一個鎖記錄(Lock Record)的空間,其中header部分用來儲存Mark Word的備份,否則執行3。
  2. JVM利用CAS操作嘗試將物件的Mark Word更新為指向鎖記錄的指標,如果成功,那麼就獲得輕量鎖,就將標誌位設定為00,執行同步程式碼塊,否則執行3。
  3. 判斷當前物件的Mark Word是否指向當前想要競爭的執行緒的鎖記錄,如果是表示則該執行緒擁有這個輕量鎖,繼續執行同步程式碼塊,也就是重入。否則,說明這個輕量鎖已經被其他執行緒擁有,那麼這個先進行自旋獲取鎖,如果一直沒有得到鎖,那麼輕量鎖則要膨脹為重量鎖(也就是將標記為設定為10),鎖標記設定為10,後面等待的執行緒則會進入阻塞狀態,如果通過自旋成功獲取了鎖,那麼輕量鎖不會膨脹為重量鎖。

釋放鎖

  1. 取出執行緒鎖記錄之前儲存的輕量鎖的Mark Word記錄,通過CAS操作將取出的記錄替換當前物件的Mark Word中
  2. 判斷當前物件的Mark Word是否指向當前執行緒的鎖記錄
  3. 如果1,2都成功,那麼就成功釋放鎖
  4. 如果1失敗,那麼就是之前有過執行緒對當前物件的鎖競爭過,但是失敗了,由輕量級鎖變為重量級鎖,導致Mark Word的結夠發生了改變。那麼後面就釋放鎖,喚醒等待的執行緒,進行新一輪的競爭。

深入瞭解Synchronized原理

重量級鎖

重量級鎖通過物件內部的監視器(monitor)實現

其中monitor的本質是依賴於底層作業系統的Mutex Lock實現

作業系統實現執行緒之間的切換需要從使用者態到核心態的切換,切換成本非常高。

鎖的升級

鎖主要存在四種狀態,無狀態鎖,偏向鎖,輕量鎖,重量鎖,會隨著執行緒競爭的程度逐漸增大。鎖只可以單向升級,不可以降級。

主要是為了提高獲得鎖和解鎖的效率。

各個狀態鎖的優缺點對比

鎖型別 特徵 優點 缺點 使用場景
偏向鎖 只需要比較ThreadId 加鎖和解鎖不需要額外的消耗,和執行非同步程式碼塊時間相差無幾 如果執行緒之間有競爭,會增加鎖撤銷的消耗 當程式大部分只有一個執行緒操作的時候
輕量鎖 自旋 競爭執行緒不會阻塞,提高了程式的響應速度 始終得不到鎖的執行緒使用自旋會消耗CPU 追求響應時間,同步執行程式碼比較快的時候
重量鎖 依賴Mutex(作業系統的互斥) 執行緒競爭不使用自旋,不怎麼會消耗CPU 執行緒阻塞,響應緩慢 同步程式碼執行比較慢的情況

最後

這裡有一張原理圖(盜用別人的圖),把上述的文字都進行了一個總結

深入瞭解Synchronized原理

參考

juejin.im/entry/58998…

blog.csdn.net/championhen…

blog.csdn.net/javazejian/…

相關文章