1. 程式人生 > 實用技巧 >[Java多執行緒]---volatile和synchronized的底層實現原理

[Java多執行緒]---volatile和synchronized的底層實現原理

文章目錄

volatile的實現原理

當有volatile變數修飾的共享變數進行寫操作的時候會多出一行有Lock字首指令的彙編程式碼。

Lock字首的指令在多核處理器下會發生兩件事情:

  1. 將當前處理器快取行的資料寫回到系統記憶體。
  2. 這個寫回記憶體的操作會使在其他CPU裡快取了該記憶體地址的資料無效。

原因:多處理器下的快取一致性協議(MESI),每個處理器通過嗅探在總線上傳播的資料來檢查自己快取的值是不是過期了,當處理器發現自己快取行對應的記憶體地址的資料被修改,就會將當前處理器的快取行設定成無效狀態,當處理器對這個資料進行修改操作的時候,會重新從系統記憶體中把資料讀到處理器快取裡。

synchronized的實現原理

JVM基於進入和退出Monitor物件來實現程式碼塊同步和方法同步,但兩者的實現細節不一樣。

1)同步程式碼塊
使用monitorenter和monitorexit指令實現。
monitorenter指令是在編譯後插入到同步程式碼塊的開始位置,monitorexit是插入到結束位置。
JVM保證每一個monitorenter必須有對應的monitorexit。
任何物件都有一個Monitor與之關聯,當且有一個Monitor被持有後,它將處於鎖定狀態。
執行緒執行到monitorenter指令時,將會嘗試獲取物件所對應的Monitor所有權,即嘗試獲取物件的鎖。

2)同步方法
在Class檔案的方法表中將該方法的access_flags欄位中設定ACC_SYNCHRONIZED標誌來實現。
在這裡插入圖片描述
物件在堆記憶體中的佈局分為三塊區域:物件頭、例項變數和填充資料,synchronized用的鎖是存在Java物件頭裡的。
物件頭中包括:

  • Mark Word,儲存物件的hashCode、分代年齡和鎖標記位,佔1字寬(32位JVM1字寬為4位元組)
  • Class Metadata Address,儲存到物件型別資料的指標 ,佔1字寬
  • Array length,如果物件是陣列,表示陣列的長度,佔1字寬

特別的,物件頭資訊是與物件自身定義的資料無關的額外儲存成本,所以為了節省空間,JVM採用了空間複用,即當物件處於不同狀態的時候,Mark Word儲存的內容是不一樣的。

每一個被鎖住的物件都會和一個Monitor關聯(物件頭的MarkWord中儲存了指向Monitor起始地址的指標),同時Monitor中有一個Owner欄位存放擁有該鎖的執行緒的唯一標識,表示該鎖被這個執行緒佔用。

在Java中是ObjectMonitor(JVM原始碼中C++實現)來實現Monitor。
ObjectMonitor中幾個關鍵欄位的含義:
_count:記錄owner執行緒獲取鎖的次數,這也決定了synchronized是可重入的。
_owner:指向擁有該物件的執行緒
_WaitSet:存放處於wait狀態的執行緒佇列
_EntryList:存放等待鎖而被block的執行緒佇列

  1. 想要獲取Monitor的執行緒先進入Monitor的_EntryList佇列阻塞等待,即遇到synchronized關鍵字時。
  2. 如果Monitor的_owner為空,則從佇列中移出並賦值給_owner。
  3. 如果在程式裡呼叫了wait方法,則該執行緒進入_WaitSet佇列。注意wait方法會釋放Monitor鎖,即將_owner賦值為null並進入_WaitSet佇列阻塞等待,這時其他在_EntryList中的執行緒就可以獲取鎖了。
  4. 當程式裡其他執行緒呼叫了notify/notifyAll方法時,就會喚醒_WaitSet中的某個執行緒,這個執行緒就會再次嘗試獲取Monitor鎖,如果成功,則就會成為Monitor的owner。
  5. 當程式裡遇到synchronized關鍵字的作用範圍結束時,就會將Monitor的owner設為null,退出。

Java中的wait/notify/notifyAll方法底層都是呼叫了ObjectMonitor的方法。