Java內存模型(JMM)詳解


在Java JVM系列文章中有朋友問為什么要JVM,Java虛擬機不是已經幫我們處理好了么?同樣,學習Java內存模型也有同樣的問題,為什么要學習Java內存模型。它們的答案是一致的:能夠讓我們更好的理解底層原理,寫出更高效的代碼。

就Java內存模型而言,它是深入了解Java並發編程的先決條件。對於后續多線程中的線程安全、同步異步處理等更是大有裨益。

硬件內存架構

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

因此,為了“壓榨”處理的性能,達到“高並發”的效果,在處理器和存儲設備之間加入了高速緩存(cache)來作為緩沖。

JMM

將運算需要使用到的數據復制到緩存中,讓運算能夠快速進行。當運算完成之后,再將緩存中的結果寫入主內存,這樣運算器就不用等待主內存的讀寫操作了。

每個處理器都有自己的高速緩存,同時又共同操作同一塊主內存,當多個處理器同時操作主內存時,可能導致數據不一致,因此需要“緩存一致性協議”來保障。比如,MSI、MESI等。

Java內存模型

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

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

JMM

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

JMM

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

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

JMM模型與硬件模型直接的對照關系可簡化為下圖:
JMM

內存之間的交互操作

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

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規范允許上面的實現,但商用虛擬機中基本都采用了原子性的操作,因此在日常使用中幾乎不會出現讀取到“半個變量”的情況。

小結

本節課重點介紹了Java內存模型以及內存交互的步驟和操作。下篇文章將重點介紹Java內存模型涉及的幾個特征和原則。歡迎關注微信公眾號“程序新視界”,第一時間獲得最新文章的更新。

原文鏈接:《Java內存模型(JMM)詳解

《面試官》系列文章:


程序新視界:精彩和成長都不容錯過
![程序新視界-微信公眾號](https://img2018.cnblogs.com/blog/1742867/201910/1742867-20191013111755842-2090947098.png)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM