JAVA內存模型(JMM)詳解

引言

爲了更好的瞭解底層原理,寫出更加高效的代碼,今天我們 詳細的分析一下JAVA內存模型(JMM),JAVA內存模型是我們深入瞭解java併發變成的先決條件。對於後續多線程 中的線程安全,同步異步處理等更是大有裨益。

硬件內存架構

在學習java內存模型之前,先了解一下計算機硬件模型,我們都知道處理器與計算機存儲設備運算速度有幾個數量級的差別。總不能讓處理器一直等待計算機存儲設備,這樣就沒有辦法顯現處理器的優勢。

因處爲了能讓處理器的性能發揮到極致,在處理器和存儲設備之間加入了高速緩存(cache)來作爲緩衝。將運算需要使用到的數據複製到緩存中,讓運算可以快速進行。當運算完成以後,再將高速緩存中的結果寫入到主內存中,這樣處理器 就不用等待主存的讀寫操作了。

每個 處理器都有自己的高速緩存,同時又共同操作同一塊主存,當多個處理器同時操作主存時,可能導致數據不一致,因此需要

“緩存一致性協議”來保障,比如,MSI,MESI等。

     

 Java內存模型

java內存模型即java memory model,簡稱JMM。 用來屏蔽掉各種硬件和操作系統的內存訪問差異,以實現java程序在各平臺下都能達到一致性的內存訪問效果。

JMM定義了線程和主內存之間的抽象關係;線程之間共享變量存儲在主內存(main memory)中,每個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是JMM的一個抽象概念,並不真實存在。他涵蓋了緩存,寄存器以及其他的硬件和編譯器優化。

          

MM與Java內存結構並不是同一個層次的內存劃分,兩者基本沒有關係。如果一定要勉強對應,那從變量、主內存、工作內存的定義看,主內存主要對應Java堆中的對象實例數據部分,工作內存則對應虛擬機棧的部分區域。

                        

主內存:主要存儲的是Java實例對象,所有線程創建的實例對象都存放在主內存中,不管該實例對象是成員變量還是方法中的本地變量(也稱局部變量),當然也包括了共享的類信息、常量、靜態變量。共享數據區域,多條線程對同一個變量進行訪問可能會發現線程安全問題。

工作內存:主要存儲當前方法的所有本地變量信息(工作內存中存儲着主內存中的變量副本拷貝),每個線程只能訪問自己的工作內存,即線程中的本地變量對其它線程是不可見的,就算是兩個線程執行的是同一段代碼,它們也會各自在自己的工作內存中創建屬於當前線程的本地變量,當然也包括了字節碼行號指示器、相關Native方法的信息。由於工作內存是每個線程的私有數據,線程間無法相互訪問工作內存,因此存儲在工作內存的數據不存在線程安全問題。

             

          JMM模型與硬件模型直接的對照關係可簡化爲上圖:

內存之間的交互操作

線程的工作內存中保存了被該線程使用到的變量的主內存副本拷貝,線程對變量的所有操作都必須在工作內存中進行,而不能直接讀寫主內存中的變量。不同的線程之間也無法直接訪問對方工作內存中的變量,線程間變量值的傳遞均需要通過主內存來完成。

                      

如上圖,本地內存A和B有主內存中共享變量x的副本,初始值都爲0。線程A執行之後把x更新爲1,存放在本地內存A中。當線程A和線程B需要通信時,線程A首先會把本地內存中x=1值刷新到主內存中,主內存中的x值變爲1。隨後,線程B到主內存中去讀取更新後的x值,線程B的本地內存的x值也變爲了1。

在此交互過程中,Java內存模型定義了8種操作來完成,虛擬機實現必須保證每一種操作都是原子的、不可再拆分的(double和long類型例外)。

  • lock(鎖定):作用於主內存的變量,它把一個變量標識爲一條線程獨佔的狀態。
  • unlock(解鎖):作用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量纔可以被其他線程鎖定。
  • read(讀取):作用於主內存的變量,它把一個變量的值從主內存傳輸到線程的工作內存中,以便隨後的load動作使用。
  • load(載入):作用於工作內存的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中。
  • use(使用):作用於工作內存的變量,它把工作內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個需要使用到變量的值的字節碼指令時將會執行這個操作。
  • assign(賦值):作用於工作內存的變量,它把一個從執行引擎接收到的值賦給工作內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作。
  • store(存儲):作用於工作內存的變量,它把工作內存中一個變量的值傳送到主內存中,以便隨後的write操作使用。
  • write(寫入):作用於主內存的變量,它把store操作從工作內存中得到的變量的值放入主內存的變量中。

如果需要把一個變量從主內存複製到工作內存,那就要順序地執行read和load操作,如果要把變量從工作內存同步回主內存,就要順序地執行store和write操作。注意,Java內存模型只要求上述兩個操作必須按順序執行,而沒有保證是連續執行。也就是說read與load之間、store與write之間是可插入其他指令的,如對主內存中的變量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操作)。

long和double型變量的特殊規則

Java內存模型要求lock,unlock,read,load,assign,use,store,write這8個操作都具有原子性,但對於64位的數據類型(long或double),在模型中定義了一條相對寬鬆的規定,允許虛擬機將沒有被volatile修飾的64位數據的讀寫操作劃分爲兩次32位的操作來進行,即允許虛擬機實現選擇可以不保證64位數據類型的load,store,read,write這4個操作的原子性,即long和double的非原子性協定。

如果多線程的情況下double或long類型並未聲明爲volatile,可能會出現“半個變量”的數值,也就是既非原值,也非修改後的值。

雖然Java規範允許上面的實現,但商用虛擬機中基本都採用了原子性的操作,因此在日常使用中幾乎不會出現讀取到“半個變量”的情況。

 

發佈了364 篇原創文章 · 獲贊 389 · 訪問量 141萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章