深刻理解JVM之Java內存模型

要了解Java內存模型,首先咱們要了解什麼是Java內存模型,它有什麼做用?
描述Java內存模型(簡稱:JMM)的規範提案JSR-133標題《Java Memory Model and Thread Specification》,經過這個標題,能夠看出JMM是和線程相關的規範。此規範地指定的 JMM Web Site 上對規範的說明以下:java

The Java Memory Model defines how threads interact through memory.

經過以上描述,說明JMM規範主要是解決在多線程場景下線程間如何通訊。數組

硬件內存架構

要了解JMM,咱們先來從硬件角度,看看多核CPU場景下,多線程程序會存在什麼問題。緩存

硬件內存架構.PNG

如上圖所示,在多核(多CPU)硬件架構中,系統中有兩個CPU,分佈運行了一個線程,對象obj保存在主內存(RAM)中。因爲RAM的速度遠低於CPU,爲了加快數據的訪問,當CPU(線程)須要使用obj對象時,會預先把obj對象加載到CPU的緩存(CPU Cache)中,處理完畢後,再把對obj對象的更新回寫到到RAM。
每一個CPU有本身獨立的緩存,一個CPU沒法訪問其餘CPU的緩存,也就是CPU間沒法直接交換數據,CPU間全部的數據交換都須要藉助主內存來完成。安全

假設線程執行的是 +1 操做。在上圖示例中,兩個線程併發執行。初始狀態,主內存中obj.num=1;線程1先讀取了obj對象,並執行+1操做,結果obj.num=2;在線程1的修改還未從CPU緩存回寫到主內存的時候,線程2從主內存中讀取了obj對象,此時線程2讀取到的obj.num=1;此後,線程1和線程2分別把obj回寫到主內存;按正常業務邏輯,obj.num被+1了兩次,結果應該是3,但上述狀況,最終主內存中obj.num=2。這是由於兩個線程對數據併發訪問衝突致使線程讀到的數據不一致。多線程

Java內存模型

Java是平臺無關的語言,爲了實現跨平臺運行,Java虛擬機(JVM)上運行的是Java字節碼(Java bytecode)。Java內存模型(Java Memory Model,JMM)是Java虛擬機規範定義的,用來屏蔽掉Java程序在各類不一樣的硬件和操做系統對內存的訪問的差別,實現Java程序在各類不一樣的平臺上都能達到內存訪問的一致性。和硬件內存架構相似,JMM把內存分爲主內存工做內存,主內存由全部線程共享,工做內存爲線程私有。
JMM規範主要定義程序變量操做的規則,規範中定義的主內存、工做內存的概念和JVM運行時內存分區中定義的堆、棧區域不是同一緯度的概念,不能互相對應,不過爲了便於理解,可把主內存類比爲堆,工做內存類比爲棧。架構

雖然工做內存和棧能夠類比,但二者是不一樣的概念。
JMM管理的程序變量,主要是指在對象實例字段、靜態字段、構成數組字段的元素等,不包括方法參數、方法局部變量等保存在棧裏的變量,由於棧自己就是線程私有的,並不存在線程一致性問題。
JMM規範規定全部的變量都要在主內存中產生,而線程不容許直接操做主內存中的變量,線程須要把變量副本拷貝到工做線程中進行操做,操做完後再回寫到主內存。

JVM內存模型.PNG

主內存
JMM規定全部的變量都必須在主內存中產生。併發

工做內存
JVM中每一個線程都有本身的工做內存,是線程私有的,能夠類比CPU的高速緩存。線程的工做內存保存了線程須要的變量在主內存中的副本。性能

數據交互接口
JMM中定義了8個用於主內存和工做內存見數據互操做的接口,用於在二者間傳輸數據,這些操做都是原子性的。優化

  1. lock(鎖定)
    做用於主內存變量,屬於互斥鎖,一個變量同時只能一個線程鎖定
  2. unlock(解鎖)
    做用於主內存變量,lock的反操做,釋放變量的鎖
  3. read(讀取)
    做用於主內存變量,表示把一個主內存變量的值傳輸到線程的工做內存,以便隨後的load操做使用
  4. load(載入)
    做用於線程工做內存變量,表示把read操做從主內存中讀取的變量的值放到工做內存的變量副本中
  5. use(使用)
    做用於線程工做內存變量,表示把工做內存中的一個變量的值傳遞給字節碼指令
  6. assign(賦值)
    做用於線程工做內存變量,表示把字節碼指令執行返回的結果賦值給工做內存中的變量,字節碼賦值操做
  7. store(存儲)
    做用於線程工做內存變量,把工做內存中的一個變量的值傳遞給主內存,以便隨後的write操做使用
  8. write(寫入)
    做用於主內存變量,把store操做從工做內存中獲得的變量的值放入主內存的變量中

數據交互原則spa

  1. 變量只能在主內存中產生。
  2. 線程對主內存變量的操做必須在線程的工做內存中進行,不能直接操做主內存中的變量。
  3. 不一樣的線程之間也不能相互訪問對方的工做內存。線程之間須要傳遞變量的值,必須經過主內存來做爲中介進行傳遞。
  4. read和load操做、store和write必須成對使用,即:不容許從主內存中讀取了變量,工做內存不接收,或者工做內存回寫了變量,主內存不接收。
  5. assign操做後的變量必須回寫到主內存。
  6. 不容許回寫沒有修改(即未assign)的變量到主內存。
  7. 一個變量同時只能被一個線程對其進行lock操做,可是同一個線程對一個變量加鎖後,能夠繼續加鎖,同時在釋放鎖的時候釋放鎖次數必須和加鎖次數相同。
  8. 對變量執行lock操做,就會清空工做空間該變量的值,使用時須要從新讀取;對一個變量執行unlock以前,必須先把變量同步回主內存中。

內存併發一致性原則

上述的內存併發一致性問題,在JMM中定義了三個原則來避免,分別是原子性、可見性和有序性。
原子性
原子性表示不可被中斷的一個或一組操做。操做一旦開始,就一直運行到結束,中間不會有任何線程切換(context switch)。

可見性
可見性是指多個線程訪問同一個變量是,一個線程修改了變量的值後,其餘線程能夠當即讀取到這個變量的最新值。

有序性
計算機在執行程序時,爲了提升性能,編譯器和處理器的經常會對指令作重排,再對亂序執行以後的結果進行重組,保證結果的正確性。也就是說在真正的執行過程當中,指令執行的順序並不必定按照代碼的書寫順序來執行,但能夠保證結果與順序執行的結果一致,這種現象成爲指令重排
從單個線程的角度來看,指令重排後指令執行順序雖然和代碼書寫順序不一致,但能夠保證執行的結果是正確的;但在多線程同時執行狀況下,指令重排可能致使工做內存和主內存同步發生延遲的現象。

volatile變量

volatile是Java中最輕量級的同步機制,JMM對volatile變量定義了特殊的操做規則,使得變量具備同步的特性,相關規則以下。

  1. 線程對volatile變量的load和use操做必須連續出現,即變量須要使用時,必須先從主內存中讀取最新值;assign和store操做也必須連續出現,即線程對變量賦值後,必須立刻寫入主內存。經過這兩點,能夠保證變量對全部線程的可見性
  2. 對volatile修飾的變量,JVM禁止指令重排優化,指令按代碼順序執行,保證代碼運行的有序性

須要注意的是,雖然volatile變量能夠保證對全部線程的可見性,可是並不能保證變量是線程安全的,多線程併發操做下,仍是會出現文章前面出現的obj.num併發衝突的問題,這是因爲變量自己 +1 操做並非原子性的,它能夠分爲兩個步驟,即變量加載到工做內存(read、load、use)、變量賦值後回寫主內存(assign、store、write),而這兩個步驟並非原子性的。A、B兩個線程的執行順序多是這樣的:

  1. 線程A讀取變量obj.num=1
  2. 線程B讀取變量obj.num=1
  3. 線程A執行+1,obj.num=1+1=2,並回寫到主內存
  4. 線程B執行+1,obj.num=1+1=2,並回寫到主內存,此時覆蓋了線程A寫入主內存的值

在這種狀況下,要保證線程間數據同步,就須要使用lock鎖住變量,這在Java語法中,表現爲 synchronized 關鍵字。

synchronized

JMM的lock和unlock操做,對應到字節碼指令是monitorenter和monitorexit兩條指令,而對應的Java代碼中,就是synchronized代碼塊或者synchronized方法。
因爲lock同時只能被一個線程獲取,因此能夠保證操做的原子性;另外lock會觸變量重讀,unlock會觸發變量回寫,因此能夠保證操做對其餘線程的可見性;另外lock保證同時只有一個線程執行對應代碼快,能夠保證操做的有效性。

參考資料

  1. JSR-133: JavaTM Memory Model and Thread Specification
  2. The Java Memory Model
相關文章
相關標籤/搜索