1. 程式人生 > 實用技巧 >Java記憶體模型與volatile關鍵字

Java記憶體模型與volatile關鍵字

目錄

一、Java記憶體模型

Java記憶體模型是為了遮蔽不同硬體和作業系統間的記憶體訪問差異,以實現Java在不同平臺下都能有一致的記憶體訪問效果。

Java記憶體模型主要定義了程式中各種變數的訪問規則,既關注虛擬機器中把變數值儲存到記憶體和從記憶體中取出變數值這樣的底層細節。(此處的變數只包括執行緒間共享的變數:例項欄位、靜態欄位和構成陣列物件的元素)

Java記憶體模型規定了所有的變數都儲存在主記憶體(虛擬機器記憶體的一部分)中。每條執行緒分別有自己的工作記憶體,工作記憶體中儲存了被該執行緒使用的變數的主記憶體副本,執行緒對變數的所有操作(讀取、修改等)都是對工作記憶體中的副本進行,而不能直接讀寫主記憶體中的資料。執行緒之間也無法直接訪問對方工作記憶體中的變數,執行緒間變數值的傳遞均需要通過主記憶體來完成。

注意:副本複製的是欄位,並不會將整個例項複製一遍,那樣太浪費空間。

執行緒、主記憶體、工作記憶體三者的互動關係如下圖所示。
在這裡插入圖片描述

Java記憶體模型與JVM記憶體模型(Java執行記憶體)

Java 記憶體模型和JVM記憶體模型是不一樣的東西。
JVM記憶體模型是指Jvm執行時將資料分割槽域儲存,強調對記憶體空間的劃分
而記憶體模型是定義了執行緒和主記憶體之間的抽象關係,即定義了JVM 在計算機記憶體(RAM)中的工作方式,如果我們要想深入瞭解Java併發程式設計,就要先理解好Java記憶體模型。

在這裡插入圖片描述

記憶體間互動操作

關於主記憶體與工作記憶體之間具體的互動協議,既一個變數如何從主記憶體拷貝到工作記憶體、如何從工作記憶體同步回主記憶體這一類的實現細節,Java記憶體模型中定義了8種操作來完成。

  • 作用於主記憶體變數的操作

    • lock(鎖定):將一個變數標識為一條執行緒獨佔的狀態。
    • unlock(解鎖):將處於鎖定狀態的變數解放出來。
    • read(讀取):將一個變數的值從主記憶體傳輸到執行緒的工作記憶體中,以便隨後的load動作使用。
    • write(寫入):把store操作從工作記憶體中得到的變數的值放入主記憶體的變數中。
  • 作用於工作記憶體變數的操作

    • load(載入):把read操作從主記憶體中得到的變數值放入工作記憶體的變數副本中。
    • use(使用):把工作記憶體中一個變數的值傳給執行引擎,每當虛擬機器遇到一個需要使用變數的值的位元組碼指令時將會執行這個操作。
    • assign(賦值):把一個從執行引擎接受的值賦給工作記憶體中的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作。
    • store(儲存):把工作記憶體中一個變數的值傳送到主記憶體中,以便隨後的write操作使用。

如果要將一個變數從主記憶體中拷貝到工作記憶體中則要順序執行read和load操作。
如果要將一個變數從工作記憶體中同步回主記憶體則要順序執行store和write操作。

二、volatile關鍵字

volatile關鍵字是Java虛擬機器提供的最輕量級的同步機制。
當一個變數被定義成volatile之後,它將具備兩項特性:可見性和有序性

可見性

1、保證此變數對所有執行緒的可見性。
這裡的可見性是指當一個執行緒修改了這個變數的值,新值對於其他執行緒來說是可以立即得知的。當寫一個volatile變數時,JMM會把該執行緒對應的本地記憶體中的值同步到主記憶體。當讀一個volatile變數時,JMM會把該執行緒對應的本地記憶體置為無效。執行緒接下來將從主記憶體中讀取共享變數。

而普通變數做不到這一點,普通變數的值線上程間傳遞時均要通過主記憶體來完成。例如:執行緒A修改了一個普通變數的值,然後向主記憶體進行回寫,另一個執行緒B需要在A回寫完成了之後再主動對主記憶體進行讀取操作,新變數才會對執行緒B可見。也就是如果執行緒B一直不讀,則資料一直不會更新。

關於volatile修飾的變數的可見性,經常會被誤認為以下描述是正確的:
“volatile變數對所有執行緒是立即可見的,對volatile變數所有的寫操作都能立刻反映到其他執行緒之中。換句話說,volatile變數在各個執行緒中是一致的,所以基於volatile的運算在併發下是執行緒安全的”。

這句話的論據部分並沒有錯,但是其並不能得出“基於volatile的運算在併發下是執行緒安全的”這個結論。
volatile變數在各個執行緒的工作記憶體中是不存在一致性問題的(每次修改都要重新整理,不存在一致性問題),但是雖然這也只能保證讀取時的一致性,Java中的運算操作符並非是原子操作,這會導致volatile變數的運算在併發下是執行緒不安全的。

例如,假設現在有多個執行緒同時對一個volatile變數a進行a++操作,那麼多個執行緒在進行a++這個操作時,會先獲取a的值,然後再進行+1的操作,在獲取a的值的時候可能別的執行緒已經對a進行了+1導致值過期。

有序性

關於Java記憶體模型中的有序性可以總結為:如果在本執行緒內觀察,所有操作都是有序的;如果在一個執行緒中觀察另一個執行緒,所有操作都是無序的。前半句是指“執行緒內表現為序列語義”,後半句是指“指令重排序”現象和“工作記憶體主主記憶體同步延遲”現象。

在Java記憶體模型中,為了效率是允許編譯器和處理器對指令進行重排序,當然重排序不會影響單執行緒的執行結果,但是對多執行緒會有影響。Java提供volatile來保證一定的有序性。另外,可以通過synchronized和Lock來保證有序性,synchronized和Lock是保證每個時刻是有一個執行緒執行同步程式碼,相當是多個單執行緒執行程式碼,自然就保證了有序性。

volatile變數的第二個特性就是有序性。它會禁止指令重排序優化。

總結

在某些情況下,volatile的同步機制的效能確實要優於鎖,但是由於虛擬機器對鎖實行的許多消除和優化,使得我們很難確切地說volatile就會比synchronized快上多少。如果讓volatile自己與自己比較,那可以確定一個原則:volatile變數讀操作的效能消耗與普通變數幾乎沒有什麼差別,但是寫操作則可能會慢上一些,因為它需要在原生代碼中插入許多記憶體屏障來保證有序性。不過即便如此,大多數場景下volatile的總開銷仍然要比鎖來得更低。我們在volatile與鎖中選擇的唯一判斷依據僅僅是volatile的語義能否滿足使用場景的需求。

參考文獻:

深入理解Java虛擬機器第3版