Java併發(1)- 聊聊Java記憶體模型

knock_小新發表於2018-07-18

引言

在計算機系統的發展過程中,由於CPU的運算速度和計算機儲存速度之間巨大的差距。為了解決CPU的運算速度和計算機儲存速度之間巨大的差距,設計人員在CPU和計算機儲存之間加入了快取記憶體來做為他們之間的橋樑,在運算時,先將資料拷貝到快取記憶體中,計算完成後再將結果寫入計算機儲存,這樣大大提高了計算效率,避免重複多次訪問計算機儲存造成的cpu資源浪費。

儘管這樣,CPU還是存在很多空閒的時間段,為了壓榨CPU的效能,多工處理誕生了,同時多工處理導致任務之間共享資源的爭搶,從而引發了併發問題。

在Java應用程式中,為了更好的解決併發問題,就必須深入理解Java記憶體模型。Java記憶體模型是Java虛擬機器非常重要的一部分,它用來指導Java虛擬機器是如何與計算機硬體記憶體之間協同工作的。在瞭解Java記憶體模型之前,我們先來看看計算機硬體記憶體模型是怎麼樣的。

硬體記憶體模型

Java併發(1)- 聊聊Java記憶體模型
現代計算機通常有2個或以上的CPU,單個CPU可能有多個核心。每個CPU核心中都包含一組暫存器,CPU在暫存器中執行操作比在計算機主儲存器中快的多。

同時每個CPU之上還存在快取記憶體,但快取記憶體的層級和位置是不固定的,現代計算機的快取層級很多達到了三級,未來可能更多。快取的位置也各有不同,有的整合了部分快取到CPU中。

同樣,快取的讀寫速度也大大快於計算機主儲存器,在暫存器和主儲存器之間,這樣的CPU--快取--主儲存的三層結構就構成了硬體記憶體模型。

CPU在程式的執行過程中,經常會頻繁的呼叫相同的資料,比如在一個迴圈內呼叫了位於另外一個實體地址的函式,這個函式可能與當前指令的物理位置相距甚遠,因為程式使用的實體記憶體並不是連續的,這就導致了需要花費很多不必要的時間在物理定址上。但如果在CPU計算之前會將所需要用到的資料先讀到快取中,計算完成之後再一次性寫入計算機主儲存器,就可以避免頻繁訪問計算機主儲存器造成的資源浪費。

Java記憶體模型

上面說了計算機的硬體記憶體模型,Java記憶體模型和硬體記憶體模型有很多類似的地方。由於存在不同的計算機作業系統型別和硬體型別,導致各種平臺下實體記憶體模型的不一致。為了讓Java上層開發有一個統一的記憶體訪問操作,保證多執行緒對共享資料的讀寫一致性,JVM規範定義了Java記憶體模型(Java Memory Model JMM)。

Java併發(1)- 聊聊Java記憶體模型
JMM通過happens-before語義(篇幅有限,後面的文章再詳細解說)定義了Java對資料的統一訪問規則。這些資料主要包括例項欄位,靜態欄位和構成陣列的元素,但不包括區域性變數、方法引數和異常處理引數,因為區域性變數和方法引數是執行緒私有的,不存在資料競爭問題。引用型別比較特殊,引用本身是執行緒私有的,但它引用的物件是可被共享的。

JMM還規定了所有的變數都儲存在主記憶體中(MainMemory),同時每個執行緒有自己的本地記憶體(LocalMeory,也叫工作記憶體),本地記憶體中儲存了所需要用到的主記憶體資料的拷貝。執行緒對變數的讀和寫都在本地記憶體中進行。

是不是發現JMM和硬體記憶體模型存在很多相似之處?主記憶體對應計算機主儲存,本地記憶體對應快取記憶體。但要知道它們雖然可以類比,卻並不是相同的東西。

本地記憶體僅僅是JMM的一個抽象概念,實際上JVM中並不存在這樣一個區域來對應,這個區域在廣義上可以包括快取、暫存器以及其他的硬體和編譯器優化等等。這句話可能聽起來比較難懂,我們只需要知道執行緒對共享變數的操作並不會直接訪問主記憶體,而是訪問一箇中間層,這個中間層包含了主記憶體中變數的拷貝,同時中間層的訪問速度大大快於訪問主記憶體的速度,在一定的操作之後將結果統一寫回主記憶體,這樣就大大提高了程式的效能。

同時也會產生另外一個問題,同一個共享變數在每一個執行緒之中都會有一份拷貝(對引用型別,並不是拷貝全部資料),產生的執行緒越多,快取開銷也就越大。

JVM記憶體模型

JVM記憶體模型定義的是執行緒堆疊和堆之間的記憶體劃分,它和Java記憶體模型是有區別的,參照《深入理解Java虛擬機器》中的解釋:

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

Java併發(1)- 聊聊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是基本型別還是引用型別都是如此。
參考資料:

  1. 《深入理解Java記憶體模型》
  2. 《深入理解Java虛擬機器》
  3. 《Java併發程式設計的藝術》
  4. http://tutorials.jenkov.com/java-concurrency/java-memory-model.html

相關文章