Java併發機制的底層實現原理

本篇博客主要是紀錄對<<Java并发编程的艺术>>的學習心得以及一些知識點的重新整理,因此文章中會有許多引用自該書的文字。同時也借鑑其他相關的技術博客,以完善整個知識框架。

volatile的應用

volatile與synchronized都是Java併發編程中的重要角色,volatile可以說是輕量級的synchronized,在多處理器開發中保證了共享變量的可見性(當一個線程修改一個共享變量時,另外一個線程能讀到這個修改的值)。volatile的執行成本也是較低的,它不會引起線程上下文的切換和調度。

volatile的定義與實現原理

Java編程語言允許線程訪問共享變量,為了確保共享變量能被準確和一致地更新,線程應該確保通過排他鎖單獨獲得這個變量。Java語言提共了volatile,在某些情況下比鎖更加方便。如果一個字段被聲明成volatile,Java線程內存模型確保所有線程看到這個變量的值是一致的。

volatile怎麼保證可見性

有volatile變量修飾的共享變量進行寫操作的時候會多出一行擁有Lock前綴的匯編代碼,此指令在多核處理器下匯引發兩件事情:

  1. 將當前處理器緩存行的數據寫回到系統內存。
  2. 這個写回內存的操作會使在其他CPU里緩存了該內存地址的數據無效。

如果對聲明了volatile的變量進行寫操作,JVM就會像處理器發送一條Lock前綴的指令,將這個變量所在緩存行的數據寫回到系統內存。但是,就算寫回到內存,如果其他處理器緩存的值還是舊的,再執行計算操作就會有問題。所以,再多處理器下,為了保證個個處理器的緩存是一致的,就會實現緩存一致性協議,每個處理器通過嗅探再總線上傳播的數據來檢查自己緩存的值是不是過期了,當處理器發現自己緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器對這個數據進行修改操作的時候,會重新從系統內存中把數據獨到處理器緩存里。

volatile的實現原則

  1. Lock前綴指令會引起處理器緩存回寫到內存: Lock前綴指令導致在執行指令期間,聲言處理器中的Lock#信號。在目前的處理器中,如果訪問的內存區域已經緩存在處理器內部,則不會聲言Lock#信號。相反,他會鎖定這塊內存區域的緩存並回寫到內存,並使用緩存一致性機制來確保修改的園子性,此操作被稱為”緩存鎖定”,緩存一致性機制會阻止同時修改由兩個以上處理器緩存的內存區域數據。
  2. 一個處理器的緩存回寫到內存會導致其他處理器的緩存無效: 處理器使用嗅探技術保證他的內部緩存、系統內存和其他處理器的緩存數據在總線上保持一致。例如,在處理器中,如果通過嗅探一個處理器來檢測其他處理器打算寫內存地址,而這個地址當前處於共享狀態,那麼正在嗅探的處理器將使它的緩存行無效,在下次訪問相同內存地址時,強制執行緩存行填充。

synchronized的實現原理與應用

很多人會稱呼synchronized為重量級鎖,但是在Java SE 1.6之後進行了各種優化,減低許多鎖開銷。接下來會介紹Java SE 1.6中為了減少性能消耗而引入的偏向鎖與輕量級鎖,以及鎖的存處結構和升級過程。

synchronized實現同步的基礎

Java中的每一個對象都可以作為鎖。具體表現為以下3種形式:

  1. 對於普通同步方法,鎖定當前實例對象。
  2. 對於靜態同步方法,鎖定當前類的Class對象。
  3. 對於同步方法塊,鎖定synchronized括號里配置的對象。

JVM規範中可以看到synchronized在JVM裡的實現原理,JVM基於進入和退出Monitor對象來實現方法同步和代碼塊同步,但兩者的實現細節不一樣。代碼塊同步釋使用monitorenter和monitorexit指令實現的,而方法同步釋使用另一種方式實現的。但是,方法的同步同樣可以使用這兩個指令來實現。

monitorenter指令是在編譯後插入到同步代碼塊的開始位置,而monitorexit是插入到方法結束處和異常處,JVM要保證每個monitorenter必須有對應的monitorexit與之配對。任何對象都有一個monitor與之關聯,且當有一個monitor被持有後,它將處於鎖定狀態。線程執行到monitorenter指令時,將會嘗試獲取對象所對應的monitor的所有權,即嘗試獲得對象的鎖。

Java對象頭

synchronized用的鎖是存在Java對象頭裡的。如果對象是數組類型,則虛擬機用3個字寬(1 Word = 4 byte = 32 bit)存處對象頭,如果是非數組類型,則用2字寬存儲對象頭。

長度 內容 說明
32/64bit Mark Word 存儲對象的hashCode或鎖信息等
32/64bit Class Metadata Address 存儲到對向類型數據的指針
32/32bit Array length 數組的長度(如果當前對象是數組)

偏向鎖

經過研究發現,大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低而引入了偏向鎖。當一個線程訪問同步塊並獲取鎖時,會在對向頭和栈幀中的鎖紀錄裡存儲偏向鎖的線程ID,以後該線程在進入和退出同步塊石不需要進行CAS操作來加鎖和解鎖,只需要簡單地測試儀下對象頭的Mark Word裡是否存儲著指向當前線程的偏向鎖。如果測試成功,表示線程已經獲得了鎖。如果測試失敗,則需要再測試一下Mark Word中偏向鎖的標識是否設置成1(表示當前是偏向鎖): 如果沒有設置,則使用CAS競爭鎖; 如果設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程。

輕量級鎖

  • 輕量級加鎖: 線程再執行同步塊之前,JVM會先再當前線程的栈幀中創建用於存儲鎖紀錄的空間,並將對象頭中的Mark Word複製到鎖紀錄中,官方稱為Displaced Mark Word。然後線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖紀錄的指針。如果成功,當前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。
  • 輕量級解鎖: 輕量級解鎖時,會使用園子的CAS操作將Displaced Mark Word替換回到對向頭,如果成功,則表示沒有競爭發生。如果失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。

鎖的優缺點對比

各類鎖的優缺點