1. 程式人生 > >Java中synchronized的實現原理與應用

Java中synchronized的實現原理與應用

Java中的每一個物件都可以作為鎖,而在Synchronized實現同步的幾種方式中分別為:

  • 普通同步方法:鎖是當前例項物件
  • 靜態同步方法:鎖是當前類的Class物件
  • 同步方法塊:鎖是Synchronized括號裡配置的物件

任何一個物件都一個Monitor與之關聯,當且一個Monitor被持有後,它將處於鎖定狀態。Synchronized在JVM裡的實現都是基於進入和退出Monitor物件來實現方法同步和程式碼塊同步,雖然具體實現細節不一樣,但是都可以通過成對的MonitorEnter和MonitorExit指令來實現。MonitorEnter指令插入在同步程式碼塊的開始位置,當代碼執行到該指令時,將會嘗試獲取該物件Monitor的所有權,即嘗試獲得該物件的鎖,而monitorExit指令則插入在方法結束處和異常處,JVM保證每個MonitorEnter必須有對應的MonitorExit。

Java物件頭

synchronized使用的鎖是存放在Java物件頭裡面,具體位置是物件頭裡面的MarkWord,MarkWord裡預設資料是儲存物件的HashCode等資訊,但是會隨著物件的執行改變而發生變化,不同的鎖狀態對應著不同的記錄儲存方式,可能值如下所示:

這裡寫圖片描述
無鎖狀態 : 物件的HashCode + 物件分代年齡 + 狀態位001

Monitor Record

Monitor Record是執行緒私有的資料結構,每一個執行緒都有一個可用monitor record列表,同時還有一個全域性的可用列表。每一個被鎖住的物件都會和一個monitor record關聯

(物件頭的MarkWord中的LockWord指向monitor record的起始地址),同時monitor record中有一個Owner欄位存放擁有該鎖的執行緒的唯一標識,表示該鎖被這個執行緒佔用。如下圖所示為Monitor Record的內部結構

Monitor Record
Owner
EntryQ
RcThis
Nest
HashCode
Candidate

Owner:初始時為NULL表示當前沒有任何執行緒擁有該monitor record,當執行緒成功擁有該鎖後儲存執行緒唯一標識,當鎖被釋放時又設定為NULL;

EntryQ:關聯一個系統互斥鎖(semaphore),阻塞所有試圖鎖住monitor record失敗的執行緒。

RcThis:表示blocked或waiting在該monitor record上的所有執行緒的個數。

Nest:用來實現重入鎖的計數。

HashCode:儲存從物件頭拷貝過來的HashCode值(可能還包含GC age)。

Candidate:用來避免不必要的阻塞或等待執行緒喚醒,因為每一次只有一個執行緒能夠成功擁有鎖,如果每次前一個釋放鎖的執行緒喚醒所有正在阻塞或等待的執行緒,會引起不必要的上下文切換(從阻塞到就緒然後因為競爭鎖失敗又被阻塞)從而導致效能嚴重下降。Candidate只有兩種可能的值0表示沒有需要喚醒的執行緒1表示要喚醒一個繼任執行緒來競爭鎖。

鎖的型別

Java SE1.6裡鎖一共有四種狀態,無鎖狀態,偏向鎖狀態,輕量級鎖狀態和重量級鎖狀態,它會隨著競爭情況逐漸升級。鎖可以升級但不能降級,目的是為了提高獲得鎖和釋放鎖的效率。

                                 無鎖 --> 偏向鎖 --> 輕量級 --> 重量級

偏向鎖

引入背景:大多數情況下鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得,為了讓執行緒獲得鎖的代價更低而引入了偏向鎖,減少不必要的CAS操作。

加鎖:當一個執行緒訪問同步塊並獲取鎖時,會在物件頭和棧幀中的鎖記錄裡儲存鎖偏向的執行緒ID,以後該執行緒在進入和退出同步塊時不需要花費CAS操作來加鎖和解鎖,而只需簡單的測試一下物件頭的Mark Word裡是否儲存著指向當前執行緒的偏向鎖,如果測試成功,表示執行緒已經獲得了鎖,如果測試失敗,則需要再測試下Mark Word中偏向鎖的標識是否設定成1(表示當前是偏向鎖),如果沒有設定,則使用CAS競爭鎖,如果設定了,則嘗試使用CAS將物件頭的偏向鎖指向當前執行緒(此時會引發競爭,偏向鎖會升級為輕量級鎖)。

膨脹過程:當前執行緒執行CAS獲取偏向鎖失敗(這一步是偏向鎖的關鍵),表示在該鎖物件上存在競爭並且這個時候另外一個執行緒獲得偏向鎖所有權。當到達全域性安全點(safepoint)時獲得偏向鎖的執行緒被掛起,並從偏向鎖所有者的私有Monitor Record列表中獲取一個空閒的記錄,並將Object設定LightWeight Lock狀態並且Mark Word中的LockRecord指向剛才持有偏向鎖執行緒的Monitor record,最後被阻塞在安全點的執行緒被釋放,進入到輕量級鎖的執行路徑中,同時被撤銷偏向鎖的執行緒繼續往下執行同步程式碼。

偏向鎖

輕量級鎖

引入背景:這種鎖實現的背後基於這樣一種假設,即在真實的情況下我們程式中的大部分同步程式碼一般都處於無鎖競爭狀態(即單執行緒執行環境),在無鎖競爭的情況下完全可以避免呼叫作業系統層面的重量級互斥鎖,取而代之的是在monitorenter和monitorexit中只需要依靠一條CAS原子指令就可以完成鎖的獲取及釋放。當存在鎖競爭的情況下,執行CAS指令失敗的執行緒將呼叫作業系統互斥鎖進入到阻塞狀態,當鎖被釋放的時候被喚醒

加鎖
(1)當物件處於無鎖狀態時(RecordWord值為HashCode,狀態位為001),執行緒首先從自己的可用moniter record列表中取得一個空閒的moniter record,初始Nest和Owner值分別被預先設定為1和該執行緒自己的標識,一旦monitor record準備好然後我們通過CAS原子指令安裝該monitor record的起始地址到物件頭的LockWord欄位,如果存在其他執行緒競爭鎖的情況而呼叫CAS失敗,則只需要簡單的回到monitorenter重新開始獲取鎖的過程即可。

(2)物件已經被膨脹同時Owner中儲存的執行緒標識為獲取鎖的執行緒自己,這就是重入(reentrant)鎖的情況,只需要簡單的將Nest加1即可。不需要任何原子操作,效率非常高。

(3)物件已膨脹但Owner的值為NULL,當一個鎖上存在阻塞或等待的執行緒同時鎖的前一個擁有者剛釋放鎖時會出現這種狀態,此時多個執行緒通過CAS原子指令在多執行緒競爭狀態下試圖將Owner設定為自己的標識來獲得鎖,競爭失敗的執行緒在則會進入到第四種情況(4)的執行路徑。

(4)物件處於膨脹狀態同時Owner不為NULL(被鎖住),在呼叫作業系統的重量級的互斥鎖之前先自旋一定的次數,當達到一定的次數時如果仍然沒有成功獲得鎖,則開始準備進入阻塞狀態,首先將rfThis的值原子性的加1,由於在加1的過程中可能會被其他執行緒破壞Object和monitor record之間的關聯,所以在原子性加1後需要再進行一次比較以確保LockWord的值沒有被改變,當發現被改變後則要重新monitorenter過程。同時再一次觀察Owner是否為NULL,如果是則呼叫CAS參與競爭鎖,鎖競爭失敗則進入到阻塞狀態。

輕量級鎖

不同鎖的比較

這裡寫圖片描述

參考