volatile與synchronized實現原理


 

------------------------------------------------------------------
  剛開始認識volatile的時候,覺得對它的一些特性非常迷惑。比如:具有可見性,如果一個線程修改了volatile變量的值,那么其它線程也會發現這一點;同時它又不具有原子性,多個線程對被volatile修飾的int 變量累加會造成相互覆蓋。這我就迷糊了:不是一個線程修改了,其它的線程中數據都無效了么,既然會重新讀取,為啥最終還會相互覆蓋呢?
volatile原理:
  我們知道:如果一個字段被聲明成volatile,java線程內存模型確保所有線程看到這個變量的值是一致的。這個就是所謂的“可見性”,就是一個線程修改了,其他線程能知道這個操作,這就是可見性。如何實現的呢?volatile修飾的變量在生成匯編代碼的時候,會產生一條lock指令,lock前綴的指令在多核處理器下會引發兩件事情:
  1、將當前處理器緩存航的數據寫回到系統內存;
  2、這個寫回內存的操作會使得在其它cpu里緩存了該內存地址的數據無效;
  這個使得其它cpu里數據無效又是怎么實現的呢?
  cpu處理數據速度是很快的,為了提高處理速度,充分發揮cpu性能,cpu不直接跟內存進行通信,而是先將數據讀入cpu高速緩存后再進行操作,但操作完不知道何時回寫到內存。如果對聲明了volatile的變量進行寫操作,jvm就會向處理器發送一條lock前綴指令,將這個變量所在緩存行的數據寫回到系統內存。但就算寫回到內存,如果其它處理器緩存的還是舊值,再執行計算操作就會有問題。所以多處理器下,為了保證各個處理器的緩存是一致的,就有了一個“緩存一致性協議”,所有硬件廠商都要按照這個標准來生產硬件。具體就是每個處理器通過嗅探在總線上傳播的數據來檢查自己緩存的值是不是過期了,當處理器發現自己緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置為無效狀態,當處理器對這個數據進行修改操作的時候,會重新從系統內存中把數據讀到處理器緩存。
注意, 如果該數據已經在別的處理器線程被修改過了,只是沒有刷新到內存,則這時候是不會重新讀數據的,而是等一下直接刷新到內存,這就造成了覆蓋的事情發生;別的線程重新讀取數據僅僅是在將變量讀到了cpu緩存,還沒有使用的時候才有的,一旦使用了,即使發現被修改了,也不會重新讀取重新計算。具有可見性,而又多線程不安全的問題就是這樣產生的。
  該部分可以結合: jvm線程模型jvm8種內存基本操作
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的內容。
   總結:為什么偏向鎖跟輕量級鎖相對來說速度快,就是因為這兩個沒有這個線程切換的過程。偏向鎖是直接自己一直在用,相當於沒有同步操作,輕量級鎖是看了下有人在用,自己覺得他們用的時間應該不長,我就死循環等一下你們吧。大概就是這么個邏輯。當然了,死循環結束,發現你丫還沒執行完,沒的說,升級成重量級鎖吧。
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM