【鎖】synchronized的實現(偏向鎖、輕量級鎖、重量級鎖)


synchronized的三種應用方式

  • 一. 修飾實例方法,作用於當前實例加鎖,進入同步代碼前要獲得當前實例的鎖。
  • 二. 修飾靜態方法,作用於當前類對象加鎖,進入同步代碼前要獲得當前類對象的鎖。
  • 三. 修飾代碼塊,指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要獲得給定對象。

synchronized的字節碼指令

synchronized同步塊使用了monitorentermonitorexit指令實現同步,這兩個指令,本質上都是對一個對象的監視器(monitor)進行獲取,這個過程是排他的,也就是說同一時刻只能有一個線程獲取到由synchronized所保護對象的監視器。

線程執行到monitorenter指令時,會嘗試獲取對象所對應的monitor所有權,也就是嘗試獲取對象的鎖,而執行monitorexit,就是釋放monitor的所有權。

synchronized的鎖的原理

兩個重要的概念:一個是對象頭,另一個是monitor

Java對象頭

在Hotspot虛擬機中,對象在內存中的布局分為三塊區域:對象頭(Mark Word、Class Metadata Address)、實例數據和對齊填充;Java對象頭是實現synchronized的鎖對象的基礎。一般而言,synchronized使用的鎖對象是存儲在Java對象頭里。它是輕量級鎖和偏向鎖的關鍵。

Mark Word
Mark Word用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標志、線程持有的
鎖、偏向線程 ID、偏向時間戳等等。Java對象頭一般占有兩個機器碼(在32位虛擬機中,1個機器碼等於4字節,
也就是32bit)。

 

 

32位JVM的Mark Word的默認存儲結構如下:

 

  25 bit 4bit 1bit
是否是偏向鎖
2bit
鎖標志位
無鎖狀態 對象的hashCode 對象分代年齡 0 01

 

Class Metadata Address
類型指針,即是對象指向它的類的元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。

Array length
如果對象是一個Java數組,那在對象頭中還必須有一塊用於記錄數組長度的數據。

Monitor

Monitor是一個同步工具,它內置於每一個Object對象中,相當於一個許可證。拿到許可證即可以進行操作,沒有拿到則需要阻塞等待。

在hotspot虛擬機中,通過ObjectMonitor類來實現monitor。

 

 

synchronized鎖的優化

jdk1.6以后對synchronized的鎖進行了優化,引入了偏向鎖、輕量級鎖,鎖的級別從低到高逐步升級: 

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

自旋鎖與自適應自旋

線程的掛起和恢復會極大的影響開銷。並且jdk官方人員發現,很多線程在等待鎖的時候,在很短的一段時間就獲得了鎖,所以它們在線程等待的時候,並不需要把線程掛起,而是讓他無目的的循環,一般設置10次。這樣就避免了線程切換的開銷,極大的提升了性能。

而適應性自旋,是賦予了自旋一種學習能力,它並不固定自旋10次一下。他可以根據它前面線程的自旋情況,從而調整它的自旋,甚至是不經過自旋而直接掛起。

鎖消除

對不會存在線程安全的鎖進行消除。

鎖粗化

如果jvm檢測到有一串零碎的操作都對同一個對象加鎖,將會把鎖粗化到整個操作外部,如循環體。

偏向鎖

多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓其獲得鎖的代價更低而引入了偏向鎖。

  • 當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID,以后該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單地測試一下對象頭的Mark Word里是否存儲着指向當前線程的偏向鎖。
  • 如果測試成功,表示線程已經獲得了鎖。
  • 如果測試失敗,則需要再測試一下Mark Word中偏向鎖的標識是否設置成01(表示當前是偏向鎖)。
  • 如果沒有設置,則使用CAS競爭鎖。
  • 如果設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程。

 

 

 

輕量級鎖

引入輕量級鎖的主要目的是在多線程競爭不激烈的情況下,通過CAS競爭鎖,減少傳統的重量級鎖使用操作系統互斥量產生的性能消耗。

輕量級鎖的執行過程:在代碼進入同步塊的時候,如果同步對象沒有被鎖定,虛擬機首先將在當前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於存儲對象目前Mark Word的拷貝,這時候線程堆棧與對象頭的狀態如圖所示:

 

 

然后虛擬機將使用CAS操作嘗試將對象頭中的Mark Word 更新為指向當前線程Lock Record的指針,如果這個更新執行成功了,那么這個線程就擁有了這個對象的鎖,並且將Mard Word中的標記位改為00,即表示該對象處於輕量級鎖狀態,這時候線程堆和對象頭的狀態如圖所示:

 

 

如果這個狀態更新失敗了,虛擬機將會檢查對象中的Mark Word 是否指向當前線程的棧幀,如果是直接進入同步代碼塊 執行,如果不是 說明有線程競爭。如果有兩條以上的線程在搶占資源,那輕量級鎖就不再有效,要膨脹為重量級鎖,鎖的狀態更改為10, Mard Word中存儲的就是指向重量級鎖的指針 后面等待的鎖就要進入阻塞狀態。

重量級鎖

重量級鎖通過對象內部的監視器(monitor)實現,其中monitor的本質是依賴於底層操作系統的Mutex Lock實現,操作系統實現線程之間的切換需要從用戶態到內核態的切換,切換成本非常高。

鎖升級

  • 偏向鎖升級輕量級鎖:當一個對象持有偏向鎖,一旦第二個線程訪問這個對象,如果產生競爭,偏向鎖升級為輕量級鎖。
  • 輕量級鎖升級重量級鎖:一般兩個線程對於同一個鎖的操作都會錯開,或者說稍微等待一下(自旋),另一個線程就會釋放鎖。但是當自旋超過一定的次數,或者一個線程在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖膨脹為重量級鎖,重量級鎖使除了擁有鎖的線程以外的線程都阻塞,防止CPU空轉。

wait和notify的原理

調用wait方法,首先會獲取監視器鎖,獲得成功以后,會讓當前線程進入等待狀態,進入等待隊列並且釋放鎖。

當其他線程調用notify后,會選擇從等待隊列中喚醒任意一個線程,而執行完notify方法以后,並不會立馬喚醒線程,原因是當前的線程仍然持有這把鎖,處於等待狀態的線程無法獲得鎖。必須要等到當前的線程執行完按monitorexit指令以后,也就是鎖被釋放以后,處於等待隊列中的線程就可以開始競爭鎖了。

wait和notify為什么需要在synchronized里面?

wait方法的語義有兩個,一個是釋放當前的對象鎖、另一個是使得當前線程進入阻塞隊列,而這些操作都和監視器是相關的,所以wait必須要獲得一個監視器鎖。

而對於notify來說也是一樣,它是喚醒一個線程,既然要去喚醒,首先得知道它在哪里,所以就必須要找到這個對象獲取到這個對象的鎖,然后到這個對象的等待隊列中去喚醒一個線程。

 

 

鎖的優缺點對比

優點 缺點 適用場景
偏向鎖 加鎖和解鎖不需要額外的消耗,和執行非同步方法比僅存在納秒級的差距 如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗 適用於只有一個線程訪問同步塊場景
輕量級鎖 競爭的線程不會阻塞,提高了程序的響應速度 如果始終得不到鎖競爭的線程使用自旋會消耗CPU 追求響應時間,鎖占用時間很短
重量級鎖 線程競爭不使用自旋,不會消耗CPU 線程阻塞,響應時間緩慢 追求吞吐量,鎖占用時間較長



參考自:

  https://blog.csdn.net/u011212394/article/details/82228321

  https://www.cnblogs.com/wade-luffy/p/5969418.html
  https://blog.csdn.net/sinat_41832255/article/details/89309944


免責聲明!

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



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