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實例對象,全部線程建立的實例對象都存放在主內存中,無論該實例對象是成員變量仍是方法中的本地變量(也稱局部變量),固然也包括了共享的類信息、常量、靜態變量。共享數據區域,多條線程對同一個變量進行訪問可能會發現線程安全問題。jvm

工做內存:主要存儲當前方法的全部本地變量信息(工做內存中存儲着主內存中的變量副本拷貝),每一個線程只能訪問本身的工做內存,即線程中的本地變量對其它線程是不可見的,就算是兩個線程執行的是同一段代碼,它們也會各自在本身的工做內存中建立屬於當前線程的本地變量,固然也包括了字節碼行號指示器、相關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)詳解

《面試官》系列文章:


程序新視界:精彩和成長都不容錯過

程序新視界-微信公衆號

相關文章
相關標籤/搜索