Java並發(1)- 聊聊Java內存模型


引言

在計算機系統的發展過程中,由於CPU的運算速度和計算機存儲速度之間巨大的差距。為了解決CPU的運算速度和計算機存儲速度之間巨大的差距,設計人員在CPU和計算機存儲之間加入了高速緩存來做為他們之間的橋梁,在運算時,先將數據拷貝到高速緩存中,計算完成后再將結果寫入計算機存儲,這樣大大提高了計算效率,避免重復多次訪問計算機存儲造成的cpu資源浪費。
盡管這樣,CPU還是存在很多空閑的時間段,為了壓榨CPU的性能,多任務處理誕生了,同時多任務處理導致任務之間共享資源的爭搶,從而引發了並發問題。
在Java應用程序中,為了更好的解決並發問題,就必須深入理解Java內存模型。Java內存模型是Java虛擬機非常重要的一部分,它用來指導Java虛擬機是如何與計算機硬件內存之間協同工作的。在了解Java內存模型之前,我們先來看看計算機硬件內存模型是怎么樣的。

硬件內存模型


現代計算機通常有2個或以上的CPU,單個CPU可能有多個內核。每個CPU內核中都包含一組寄存器,CPU在寄存器中執行操作比在計算機主存儲器中快的多。
同時每個CPU之上還存在高速緩存,但高速緩存的層級和位置是不固定的,現代計算機的緩存層級很多達到了三級,未來可能更多。緩存的位置也各有不同,有的集成了部分緩存到CPU中。 同樣,緩存的讀寫速度也大大快於計算機主存儲器,在寄存器和主存儲器之間,這樣的CPU--緩存--主存儲的三層結構就構成了硬件內存模型。
CPU在程序的執行過程中,經常會頻繁的調用相同的數據,比如在一個循環內調用了位於另外一個物理地址的函數,這個函數可能與當前指令的物理位置相距甚遠,因為程序使用的物理內存並不是連續的,這就導致了需要花費很多不必要的時間在物理尋址上。但如果在CPU計算之前會將所需要用到的數據先讀到緩存中,計算完成之后再一次性寫入計算機主存儲器,就可以避免頻繁訪問計算機主存儲器造成的資源浪費。

Java內存模型

上面說了計算機的硬件內存模型,Java內存模型和硬件內存模型有很多類似的地方。由於存在不同的計算機操作系統類型和硬件類型,導致各種平台下物理內存模型的不一致。為了讓Java上層開發有一個統一的內存訪問操作,保證多線程對共享數據的讀寫一致性,JVM規范定義了Java內存模型(Java Memory Model JMM)。

JMM通過happens-before語義(篇幅有限,后面的文章再詳細解說)定義了Java對數據的統一訪問規則。這些數據主要包括實例字段,靜態字段和構成數組的元素,但不包括局部變量、方法參數和異常處理參數,因為局部變量和方法參數是線程私有的,不存在數據競爭問題。
引用類型比較特殊,引用本身是線程私有的,但它引用的對象是可被共享的。
JMM還規定了所有的變量都存儲在主內存中(Main Memory),同時每個線程有自己的本地內存(Local Meory,也叫工作內存),本地內存中保存了所需要用到的主內存數據的拷貝。線程對變量的讀和寫都在本地內存中進行。
是不是發現JMM和硬件內存模型存在很多相似之處?主內存對應計算機主存儲,本地內存對應高速緩存。但要知道它們雖然可以類比,卻並不是相同的東西。
本地內存僅僅是JMM的一個抽象概念,實際上JVM中並不存在這樣一個區域來對應,這個區域在廣義上可以包括緩存、寄存器以及其他的硬件和編譯器優化等等。這句話可能聽起來比較難懂,我們只需要知道線程對共享變量的操作並不會直接訪問主內存,而是訪問一個中間層,這個中間層包含了主內存中變量的拷貝,同時中間層的訪問速度大大快於訪問主內存的速度,在一定的操作之后將結果統一寫回主內存,這樣就大大提高了程序的性能。
同時也會產生另外一個問題,同一個共享變量在每一個線程之中都會有一份拷貝(對引用類型,並不是拷貝全部數據),產生的線程越多,緩存開銷也就越大。

JVM內存模型

JVM內存模型定義的是線程堆棧和堆之間的內存划分,它和Java內存模型是有區別的,參照《深入理解Java虛擬機》中的解釋:

這兩者本沒有關系。如果一定要勉強對應,那從變量、主內存、工作內存的定義來看,主內存主要對應於Java堆中的對象實例數據部分,而工作內存則對應於虛擬機棧中的部分區域。從更低層次上說,主內存就是物理內存,而為了獲取更好的執行速度,虛擬機(甚至是硬件系統本身的優化措施)可能會讓工作內存優先存儲於寄存器和高速緩存中,因為運行時主要訪問——讀寫的是工作內存。

所有的原始類型(boolean,byte,short,char,int,long,float,double)局部變量都存儲在線程堆棧中,不對其他線程共享。堆中則包含了Java程序中創建的對象。
舉個例子:

public class MemoryModel {
	
	public int i = 0;
	
	public void methodOne() {
		
		int localVarOne = 1;
		
		SharedObject localVarTwo = SharedObject.sharedObject;
		
		Integer localVarThree = new Integer(1);
	}
}

public class SharedObject {
	
	pubic static SharedObject sharedObject = new SharedObject();
	
	public int sharedVarOne = 1;
}

代碼中局部變量localVarOne存儲在線程堆棧中。局部變量localVarTwo的引用存儲在線程堆棧中,但對象本身存儲在堆上。局部變量localVarThree同localVarTwo一樣,引用存儲在線程堆棧中,但對象本身存儲在堆上。不同的是多線程執行methodOne方法時,localVarTwo由於是靜態類型,在堆中只有一份數據,而localVarThree在堆和堆棧中都有多份數據。局部變量對象的成員變量sharedVarOne也存儲在堆上,無論sharedVarOne是基本類型還是引用類型都是如此。


參考資料:
《深入理解Java內存模型》
《深入理解Java虛擬機》
《Java並發編程的藝術》
http://tutorials.jenkov.com/java-concurrency/java-memory-model.html


免責聲明!

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



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