同步和對象鎖-Synchronization and Object Locking

    Java語言的一個主要強項就是對多線程程序的內置支持。一個對象在多個線程之間共享讀取時可以通過加鎖來實現有序同步的訪問。Java提供原語來指明關鍵代碼區域,原語作用於共享對象,並保證同一時刻只有一個線程能執行這些代碼。第一個進入代碼區域的線程鎖定共享對象,當與這個共享對象相關的第二個線程要進入相同的代碼區域時,它只能等待,直到第一個線程解鎖這個共享對象。
    在Java官方的HotSpot虛擬機中,每一個對象前面部分都是由對象頭(HeaderWord)、對Class的指針組成。對象頭中保存了HashCode以及分代垃圾收集的標識位。另外,對象頭還可以用來實現輕量級鎖。下圖顯示了對象頭的組成和不同狀態下的表現形式。

    圖的右邊部分說明了標準加鎖流程。一般來說,一個未被加鎖的對象的HeaderWord的最後兩位的值是01。當對象的某個方法同步執行時,當前線程的當前棧幀中會創建一個鎖記錄(LockRecord),鎖記錄包括從HeaderWord複製而來的內容,以及一個對該對象的引用,如下圖:

【改天畫圖,睡覺去了】

然後JVM會嘗試使用CAS操作修改對象的HeaderWord,使HeaderWord指向棧幀中的鎖記錄。如果修改成功,則該線程隨即擁有鎖。HeaderWord的最後兩位是00,將對象標識爲已加鎖,剩餘前面的bit都用於表示鎖記錄。
    如果CAS操作因爲對象已被鎖定而失敗,JVM首先檢查HeaderWord是否指向當前線程的方法棧。如果是,說明當前線程已經擁有該對象的鎖並且可以安全地繼續執行。對於一個被遞歸鎖定的對象,棧幀中的鎖記錄置爲0而不是HeaderWord。如果兩個線程併發地競爭同一個鎖對象,輕量級鎖應該膨脹爲重量級鎖,以便管理這些等待的線程。
    儘管輕量級鎖的代價比重量級鎖要小得多,但CAS操作都是在多核CPU的計算機上自動執行的,所以輕量級鎖的性能也可能會變差,即使大多數對象只是被某一個線程加鎖和解鎖。在Java 6中,這個缺點由通過所謂的‘自由存儲偏向鎖技術’[RuSele06]得以解決,它使用類似於[KaajiaYa02]的概念。第一次加鎖會執行CAS操作並在HeaderWord中設置線程ID,可以看做該對象偏向這個線程,接下來該線程的加鎖和解鎖不需要原子操作或設置任何HeaderWord。該線程的棧幀中也不會有鎖記錄,因爲一個已偏向的對象不需要檢查鎖記錄。
    當一個線程競爭一個已經偏向其它線程的鎖對象時,會撤銷偏向鎖,使該對象看起來像是正常(公平)地實現了加鎖。具體做法是,遍歷被偏向的線程的棧,根據輕量級鎖的方案調整該對象的鎖記錄,對象的HeaderWord會指向最早的鎖記錄,這時所有的線程都要掛起。另外,當對象的HashCode生成後,偏向鎖也必須要撤銷,線程ID的位置改爲存儲HashCode,因爲在HeaderWord中兩者共用同一塊存儲區域。
    按照多線程共享的用途設計的對象(比如生產者/消費者隊列),不適合使用偏向鎖。因此,當一個類的實例頻繁地銷燬,那麼該類的偏向鎖會被禁用,這被稱爲‘批量銷燬’。如果在一個被禁用了偏向鎖的類實例上調用加鎖的代碼,將執行標準的輕量級鎖,新創建的實例都會標記爲‘不可偏向的’。
    還存在一個叫做‘批量重新偏向’的相似機制,是針對類的對象被不同的線程加鎖/解鎖但沒有併發的場景的優化。它在沒有禁用偏向鎖的情況下使類所有實例的偏向鎖都失效。類中的epoch值像時間戳一樣來標記偏向鎖的有效性。在對象創建時把該值複製到HeaderWord中,相應類中的epoch值以遞增的形式高效地實現批量重新偏向。下一次對此類的對象加鎖時,代碼在頭字中檢測到不同的值,會將對象重新偏向到當前線程。

    本文翻譯自https://wiki.openjdk.java.net/display/HotSpot/Synchronization ,by@六噸代碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章