1. 程式人生 > >volatile與synchronized實現原理

volatile與synchronized實現原理

------------------------------------------------------------------   剛開始認識volatile的時候,覺得對它的一些特性非常迷惑。比如:具有可見性,如果一個執行緒修改了volatile變數的值,那麼其它執行緒也會發現這一點;同時它又不具有原子性,多個執行緒對被volatile修飾的int 變數累加會造成相互覆蓋。這我就迷糊了:不是一個執行緒修改了,其它的執行緒中資料都無效了麼,既然會重新讀取,為啥最終還會相互覆蓋呢? volatile原理:   我們知道:如果一個欄位被宣告成volatile,java執行緒記憶體模型確保所有執行緒看到這個變數的值是一致的。這個就是所謂的“可見性”,就是一個執行緒修改了,其他執行緒能知道這個操作,這就是可見性。如何實現的呢?volatile修飾的變數在生成彙編程式碼的時候,會產生一條lock指令,lock字首的指令在多核處理器下會引發兩件事情:   1、將當前處理器快取航的資料寫回到系統記憶體;   2、這個寫回記憶體的操作會使得在其它cpu裡快取了該記憶體地址的資料無效;   這個使得其它cpu裡資料無效又是怎麼實現的呢?   cpu處理資料速度是很快的,為了提高處理速度,充分發揮cpu效能,cpu不直接跟記憶體進行通訊,而是先將資料讀入cpu快取記憶體後再進行操作,但操作完不知道何時回寫到記憶體。如果對聲明瞭volatile的變數進行寫操作,jvm就會向處理器傳送一條lock字首指令,將這個變數所在快取行的資料寫回到系統記憶體。但就算寫回到記憶體,如果其它處理器快取的還是舊值,再執行計算操作就會有問題。所以多處理器下,為了保證各個處理器的快取是一致的,就有了一個“快取一致性協議”,所有硬體廠商都要按照這個標準來生產硬體。具體就是每個處理器通過嗅探在總線上傳播的資料來檢查自己快取的值是不是過期了,當處理器發現自己快取行對應的記憶體地址被修改,就會將當前處理器的快取行設定為無效狀態,當處理器對這個資料進行修改操作的時候,會重新從系統記憶體中把資料讀到處理器快取。 注意,如果該資料已經在別的處理器執行緒被修改過了,只是沒有重新整理到記憶體,則這時候是不會重新讀資料的,而是等一下直接重新整理到記憶體,這就造成了覆蓋的事情發生
;別的執行緒重新讀取資料僅僅是在將變數讀到了cpu快取,還沒有使用的時候才有的,一旦使用了,即使發現被修改了,也不會重新讀取重新計算。具有可見性,而又多執行緒不安全的問題就是這樣產生的。 synchronized原理:   synchronized是用java的monitor機制來實現的,就是synchronized程式碼塊或者方法進入及退出的時候會生成monitorenter跟monitorexit兩條命令。執行緒執行到monitorenter時會嘗試獲取物件所對應的monitor所有權,即嘗試獲取的物件的鎖;monitorexit即為釋放鎖。   monitor機制是跟java物件結構相關的。HotSpot虛擬機器中,物件在記憶體中儲存的佈局可以分為三塊區域:物件頭,例項資料跟對齊填充。

從上面的這張圖裡面可以看出,物件在記憶體中的結構主要包含以下幾個部分:
  • Mark Word(標記欄位):物件的Mark Word部分佔4個位元組,其內容是一系列的標記位,比如輕量級鎖的標記位,偏向鎖標記位等等。
  • Klass Pointer(Class物件指標):Class物件指標的大小也是4個位元組,其指向的位置是物件對應的Class物件(其對應的元資料物件)的記憶體地址
  • 物件實際資料:這裡麵包括了物件的所有成員變數,其大小由各個成員變數的大小決定,比如:byte和boolean是1個位元組,short和char是2個位元組,int和float是4個位元組,long和double是8個位元組,reference是4個位元組
  • 對齊:最後一部分是對齊填充的位元組,按8個位元組填充。
  • 其實,如果是陣列物件,頭資訊還包括一個Array length的內容,用來記錄陣列長度。
  我們看這個Mark Word,它包含物件的hashcode,分代年齡跟鎖標記位3部分。具體結構如下(32位虛擬機器):    64位虛擬機器下,Mark Word是64bit的,存出結果如下:   這麼複雜的結構,跟synchronized有什麼關係呢?   當然有關係,synchronized就是利用以上結構來實現的,每次就是搶佔上邊的Mark Word,然後修改裡邊各個小段的內容;然後,jdk的開發人員經過研究發現,大多數情況下,鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得,那我們老是搶來搶去的豈不是沒意義。於是考慮進行優化,也就有了偏向鎖,輕量級鎖以及重量級鎖的概念。然後接下來我們來看這三種鎖究竟是怎麼一回事兒。   偏向鎖:   簡單的講,就是在鎖物件的物件頭中有個ThreaddId欄位,這個欄位如果是空的,   第一次獲取鎖的時候,就將自身的ThreadId寫入到鎖的ThreadId欄位內,將鎖頭內的是否偏向鎖的狀態位置1.   這樣下次獲取鎖的時候,直接檢查ThreadId是否和自身執行緒Id一致,如果一致,則認為當前執行緒已經獲取了鎖,因此不需再次獲取鎖,略過了輕量級鎖和重量級鎖的加鎖階段。提高了效率。   但是偏向鎖也有一個問題,就是當鎖有競爭關係的時候,需要解除偏向鎖,使鎖進入競爭的狀態。 上圖中只講了偏向鎖的釋放,其實還涉及偏向鎖的搶佔,其實就是兩個程序對鎖的搶佔,在synchrnized鎖下表現為輕量鎖方式進行搶佔。 注:也就是說一旦偏向鎖衝突,雙方都會升級為輕量級鎖。(這一點與輕量級->重量級鎖不同,那時候失敗一方直接升級,成功一方在釋放時候notify)   輕量級鎖:   之後會進入到輕量級鎖階段,兩個執行緒進入鎖競爭狀態(注,我理解仍然會遵守先來後到原則;注2,的確是的,下圖中提到了mark word中的lock record指向堆疊中最近的一個執行緒的lock record),一個具體例子可以參考synchronized鎖機制。   每一個執行緒在準備獲取共享資源時:   第一步,檢查MarkWord裡面是不是放的自己的ThreadId ,如果是,表示當前執行緒是處於 “偏向鎖” ;   第二步,如果MarkWord不是自己的ThreadId,鎖升級,這時候,用CAS來執行切換,新的執行緒根據MarkWord裡面現有的ThreadId,通知之前執行緒暫停,之前執行緒將Markword的內容置為空。   第三步,兩個執行緒都把物件的HashCode複製到自己新建的用於儲存鎖的記錄空間,接著開始通過CAS操作,把共享物件的MarKword的內容修改為自己新建的記錄空間的地址的方式競爭MarkWord;   第四步,第三步中成功執行CAS的獲得資源,失敗的則進入自旋 第五步,自旋的執行緒在自旋過程中,成功獲得資源(即之前獲的資源的執行緒執行完成並釋放了共享資源),則整個狀態依然處於 輕量級鎖的狀態,如果自旋失敗 第六步,進入重量級鎖的狀態,這個時候,自旋的執行緒進行阻塞,等待之前執行緒執行完成並喚醒自己;   重量級鎖:   這個就是我們平常說的synchronized鎖,如果搶佔不到,則執行緒阻塞,等待正在執行的執行緒結束後喚醒自己,然後重新開始競爭。之所以說是重量級,是因為執行緒阻塞會讓出cpu資源,從核心態轉換為使用者態,然後執行的時候再次轉換為核心態,這個過程中,cpu要切換執行緒,看這個執行緒上次執行到哪兒了,這次應該從哪兒開始,相關的變數有哪些之類的,這就是所謂的執行上下文,這個上下文的切換對寶貴的cpu資源來說是“無用功”,因為這是在為執行做準備條件,如果cpu大量的時間用在這些“無用功”上,當然也就出活兒少,也就影響執行效率了。 synchronized就是這樣,預設開始是偏向鎖,有競爭就逐漸升級,最終可能是重量級鎖的一個過程。鎖定的區域就是物件的Mark Word的內容。   總結:為什麼偏向鎖跟輕量級鎖相對來說速度快,就是因為這兩個沒有這個執行緒切換的過程。偏向鎖是直接自己一直在用,相當於沒有同步操作,輕量級鎖是看了下有人在用,自己覺得他們用的時間應該不長,我就死迴圈等一下你們吧。大概就是這麼個邏輯。當然了,死迴圈結束,發現你丫還沒執行完,沒的說,升級成重量級鎖吧。