要了解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場景下,多線程程序會存在什麼問題。緩存
如上圖所示,在多核(多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虛擬機(JVM)上運行的是Java字節碼(Java bytecode)。Java內存模型(Java Memory Model,JMM)是Java虛擬機規範定義的,用來屏蔽掉Java程序在各類不一樣的硬件和操做系統對內存的訪問的差別,實現Java程序在各類不一樣的平臺上都能達到內存訪問的一致性。和硬件內存架構相似,JMM把內存分爲主內存和工做內存,主內存由全部線程共享,工做內存爲線程私有。
JMM規範主要定義程序變量操做的規則,規範中定義的主內存、工做內存的概念和JVM運行時內存分區中定義的堆、棧區域不是同一緯度的概念,不能互相對應,不過爲了便於理解,可把主內存類比爲堆,工做內存類比爲棧。架構
雖然工做內存和棧能夠類比,但二者是不一樣的概念。
JMM管理的程序變量,主要是指在對象實例字段、靜態字段、構成數組字段的元素等,不包括方法參數、方法局部變量等保存在棧裏的變量,由於棧自己就是線程私有的,並不存在線程一致性問題。
JMM規範規定全部的變量都要在主內存中產生,而線程不容許直接操做主內存中的變量,線程須要把變量副本拷貝到工做線程中進行操做,操做完後再回寫到主內存。
主內存
JMM規定全部的變量都必須在主內存中產生。併發
工做內存
JVM中每一個線程都有本身的工做內存,是線程私有的,能夠類比CPU的高速緩存。線程的工做內存保存了線程須要的變量在主內存中的副本。性能
數據交互接口
JMM中定義了8個用於主內存和工做內存見數據互操做的接口,用於在二者間傳輸數據,這些操做都是原子性的。優化
數據交互原則spa
上述的內存併發一致性問題,在JMM中定義了三個原則來避免,分別是原子性、可見性和有序性。
原子性
原子性表示不可被中斷的一個或一組操做。操做一旦開始,就一直運行到結束,中間不會有任何線程切換(context switch)。
可見性
可見性是指多個線程訪問同一個變量是,一個線程修改了變量的值後,其餘線程能夠當即讀取到這個變量的最新值。
有序性
計算機在執行程序時,爲了提升性能,編譯器和處理器的經常會對指令作重排,再對亂序執行以後的結果進行重組,保證結果的正確性。也就是說在真正的執行過程當中,指令執行的順序並不必定按照代碼的書寫順序來執行,但能夠保證結果與順序執行的結果一致,這種現象成爲指令重排。
從單個線程的角度來看,指令重排後指令執行順序雖然和代碼書寫順序不一致,但能夠保證執行的結果是正確的;但在多線程同時執行狀況下,指令重排可能致使工做內存和主內存同步發生延遲的現象。
volatile是Java中最輕量級的同步機制,JMM對volatile變量定義了特殊的操做規則,使得變量具備同步的特性,相關規則以下。
須要注意的是,雖然volatile變量能夠保證對全部線程的可見性,可是並不能保證變量是線程安全的,多線程併發操做下,仍是會出現文章前面出現的obj.num併發衝突的問題,這是因爲變量自己 +1
操做並非原子性的,它能夠分爲兩個步驟,即變量加載到工做內存(read、load、use)、變量賦值後回寫主內存(assign、store、write),而這兩個步驟並非原子性的。A、B兩個線程的執行順序多是這樣的:
在這種狀況下,要保證線程間數據同步,就須要使用lock鎖住變量,這在Java語法中,表現爲 synchronized
關鍵字。
JMM的lock和unlock操做,對應到字節碼指令是monitorenter和monitorexit兩條指令,而對應的Java代碼中,就是synchronized代碼塊或者synchronized方法。
因爲lock同時只能被一個線程獲取,因此能夠保證操做的原子性;另外lock會觸變量重讀,unlock會觸發變量回寫,因此能夠保證操做對其餘線程的可見性;另外lock保證同時只有一個線程執行對應代碼快,能夠保證操做的有效性。
參考資料