JVM -Java內存模型以及內存屏障深入解讀(Java merory model)

1.Java內存模型

1.1主內存與工作內存

java內存模型的主要目的是定義程序中各種變量的訪問規則,即關注在虛擬機中把變量存儲到內存和從內存中取出變量值這樣的底層細節。此處的變量(Variables)與Java編程中所說的變量有所區別,它包括了實例字段、靜態字段和構成數組對象的元素,但是不包括局部變量與方法參數,因爲後者是線程私有的不會被共享,自然就不會存在競爭問題。

主內存(Main memory):此處的主內存與物理硬件得主內存名字一樣,兩者也可以互相類比,但此處僅是虛擬機內存得一部分。

工作內存(Working Memory ):每條線程還有自己的工作內存(可與高速緩存類比),線程的工作內存保存了被該線程使用到的變量的主內存得副本拷貝,線程對變量的所有操作(讀取、賦值等)都必須在工作內存進行,而不能直接讀寫主內存中的變量。不同線程之間也無法直接訪問對方工作內存中的變量,線程間變量的值傳遞均需要通過主內存來完成。

這裏說的主內存、工作內存與JVM中Java堆、棧、方法區等並不是一個層次的內存劃分,如果勉強對比,從變量、主內存、工作內存定義來看,主內存主要對應Java堆中的對象實例數據部分,而工作內存則對應虛擬機棧中的部分區域。

1.2 內存間交互操作

關於主內存與工作內存之間具體的交互協議,即一個變量如何從主內存拷貝到工作內存、如何從工作內存同步回主內存得實現細節。Java內存模型定義了8種操作來完成,且虛擬機實現時必須保證是原子的、不可再分的(double,long 類型除外)。

  • lock(鎖定):作用與主內存得變量,它把一個變量標識爲一跳線程獨佔狀態

  • unlock(解鎖):作用於主內存得變量,它把一個處於鎖定狀態的變量釋放出來,釋放後得變量纔可以被其他線程鎖定

  • read(讀取):作用與主內存得變量,它把一個變量的值從主內存傳輸到線程的工作內存中,以便隨後的load動作使用

  • load(載入):作用於工作內存得變量,它把read操作從主內存中得到的變量值放入到工作內存得變量副本中。

  • use(使用):作用於工作內存得變量,它把工作內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個需要使用到變量的值的字節碼指令時將會執行這個操作

  • assign(賦值):作用於工作內存得變量,它把一個從執行引擎收到的值賦值給工作內存得每個變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作。

  • store(存儲):作用於工作內存得變量,它把工作內存中一個變量的值傳送到主內存中,以便隨後的write操作使用

  • write(寫入):作用於主內存得變量,它把store操作從工作內存得到的變量的值放入到主內存得變量中。

      如果要把一個變量從主內存複製到工作內存,那就要順序地執行read和load操作,反之如果要將工作內存得變量同步回主內存,就要順序得執行store,write操作。注意:這裏說的是這兩個操作必須順序執行,而並不要求是連續操作,也就是說read,load之間,store,wirte之間是可以插入其他指令得,如堆內存變量a,b進行訪問時,一種可能得順序是read a 、read b,load b 、load a,除此之外,Java內存模型還規定了在執行上訴8中基本操作時必須要滿足如下規則:

  • 不允許read 和load ,store 和write操作之一單獨出現,即不允許一個變量從主內存存讀取了但是工作內存不接受或者從工作內存發起回寫了但主內存不接受的情況出現

  • 不允許一個線程丟棄它得最近的assign操作,即變量在工作內存中改變了之後必須把該變化同步回主內存。

  • 不允許一個線程無原因地(沒有發生過任何assign操作),把數據從線程的工作內存同步回主內存中

  • 一個新的變量只能在主內存中“誕生”,不允許在工作內存中直接使用一個未被初始化(load 或者assign)的變量。換句話說,就是對一個變量實施use、store操作之前,必須先執行過了assign和load操作。

  • 一個變量在同一個時刻只允許一條線程對其進行lock操作,但lock操作可以被同一條線程重複執行多次,多次執行lock後,只有執行相同次數的unlock操作,變量纔會被解鎖

  • 如果一個變量執行lock操作,那將回清空工作內存中此變量的值,在執行引擎使用這個變量前,需要重新執行load 或者assign操作初始化變量的值。

  • 如果一個變量事先沒有被lock操作鎖定,那就不允許對他執行unlock操作,也不允許去unlock一個被其他線程鎖定住的變量。

  • 對一個變量執行unlock操作前,必須把該變量同步回主內存中(store,write操作)

1.3volatile 型變量的特殊規則

被volatile修飾的變量具有兩層特性

  • 可見性:當一條線程修改了這個變量的值,新值對於其他線程來說是可以立即得知的,而普通變量是做不到這一點的。普通變量的值在線程間傳遞均需通過主內存來完成,例如線程A修改一個普通變量值後項主內存進行回寫,另一個線程B在線程A回寫完成了之後再從主內存讀取操作,新變量值纔會堆線程B可見。

關於voatile變量的可見性,經常回被誤解爲“volatile 變量對所有線程是立即可見的,對volatile變量所有寫操作都能立刻反應到其他線程中,換句話說volatile變量在各個線程中是一致的,所以volatile變量的運算在併發下是安全的”。這句話論據沒有錯,但是並不能得出這樣的結論,原因是Java裏面的運算並非原子性操作,導致volatile變量也可以在併發下一樣是不安全的。

由於volatile變量只能保證變量的當前值,在不符合以下兩條規則運算場景中,我們依然要通過加鎖來確保原子性。

  • 運算結果並不不依賴變量的當前值,或者能夠確保只有單一的線程修改變量的值

  • 變量不需要與其他的狀態變量共同參與不變約束

  • 禁止指令重排序優化:普通變量僅僅保證在該方法的執行過程中所有依賴賦值結果的地方都能獲取到正確的結果,而不能保證變量賦值操作得順序與程序代碼的執行順序一致,因爲在一個線程的方法執行過程中無法感知到這一點,這也就是java 內存模型中描述的所謂的“線程內表現未串行的語義”

1.4重排序

在執行程序時爲了提高性能、編輯器和處理器經常會對指令進行重排序。重排序分爲三種類型:

  • 編輯器優化的重排序。編譯器在不改變單線程語義前提下,可以重新安排語句的執行順序。
  • 指令級別的重排序:現在處理器採用了指令級並行技術來將多條指令重疊執行,如果不存在數據依賴性,處理器可以該你那嗎語句對應機器指令的執行順序。
  • 內存系統的重排序:由於處理器使用緩存和讀寫緩衝區,這使得家在和存儲看上去可能是在亂序執行/

Java源代碼到最終執行的指令順序,會經過下面三種重排序:

爲了保證內存的可見性,Java編譯器在生成指令序列的適當位置插入內存屏障來禁止特定類型的處理器重排序。Java內存模型把內存屏障分爲LoadLoad、LoadStore、StoreLoad、StoreStore四種。

屏障類型 指令示例 說明
LoadLoad Barriers Load 1;LoadLoad;Load2 確保Load1數據的裝載,之前與Load2以及所有後續裝載指令的裝載
StoreStore Barries

Store1;StoreStore;

Store2

確保Store數據對其他處理器可見(刷到內存),之前於Store2以及後續存儲指令的存儲
LoadStore Barriers

Load1;

LoadStore;

Store2

確保Load1數據裝載,之前與Store2以及所有後續的存儲指令刷新到內存
StoreLoad Barriers

Store2

StoreLoad

Load2

確保Store1數據對其他處理器變得可見(指刷新到內存),之前與Load2以及所有後續裝載指令的裝載。StoreLoad Barrires 會使改屏障之前的所有內存訪問指令(存儲和裝載指令)完成之後,才執行該屏障之後的內存訪問指令。

 

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