天天看點

Java記憶體模型

Java記憶體模型是Java Memory Model的縮寫,又簡稱為JMM,是一個抽象的概念。Java記憶體模型的存在主要是用來屏蔽不同硬體平台通路記憶體的差異。使它們讓Java程式在不同的平台下通路記憶體達到一緻的效果。在JVM内部,我們姑且分為堆和棧兩部分。當線程建立的時候,JVM會為其建立一個工作記憶體來存儲線程的私有資料,線程對變量的操作都會先從主記憶體拷貝一份到自己的工作記憶體當中,進行一系列的運算,然後再将運算結果更新到主記憶體當中,不能直接對主記憶體進行操作。線程間的通信(Thread-A和Thread-B), 必須通過主記憶體完成,它們之間是無法直接通路對方的工作記憶體。記憶體模型與系統記憶體架構關系如下:

Java記憶體模型

通過上圖,我們對Java記憶體模型的工作流程有了一個大緻的了解。學過JVM原理的同學可能對Java記憶體模型跟JVM運作時的資料區搞混,在JVM裡記憶體可能又細分成方法區、虛拟機棧、本地方法棧、程式計數器和堆這五部分,其實它們本質上沒什麼差別,隻是從不同次元上去劃分,就像文章開頭說的那樣,JMM是一個抽象的概念。下面我們再來聊聊,經常聽到的并發過程中遇到的幾個要處理的特性,原子性、有序性和可見性。

1.原子性

Java中的原子性指的是對基本資料類型的讀取和指派操作是原子操作,這個操作是不可中斷和分割,要麼全部執行,要麼全部不執行。咋一看資料庫中的事務還挺像。但值得我們注意的是32位的JVM平台對Long和double兩種資料類型的讀寫不是原子操作,這個我們很容易了解,因為32位的平台,每次讀寫是32位的存儲單元,Long和double占64位,如果是多個線程讀寫,一個線程讀完前32位存儲單元,剛好另一個線程讀取後32位存儲單元,這違背了原子的特性。不過JVM的不斷完善,估計這個問題我們可以忽略不計。

2.可見性

可見性指的是多個線程同時通路一個變量的時候,如果一個線程對變量進行了修改,其它的線程可以看到這個變量的改變。當然對于串行的程式來說,這個可見性就可以忽略了,因為任何一個操作修改變量的值,後續的操作都能看到這個變量在變化。對于可見性,Java提供了volatile關鍵字來保證可見性。關于volatile關鍵字的講解,請參照《Java記憶體模型的了解》一文。

3.有序性

有序性顧名思義就是按照順序執行,也就是按照代碼先後順序進行執行。

happens-before

關于有序性,除了通過volatile關鍵字來保證一定"有序性"外,我們還可以用Lock和synchronized來保證有序性,當然Java記憶體模型本身也可以做到一定"有序性",這就是大家老生常談的happens-before原則。兩個操作,如果如果一個操作執行的結果需要對另一個操作可見,那麼這兩個操作之間必須存在happens-before關系。舉個例子咱們了解下

int x=0; // Thread-A 
int y=x; // Thread-B
           

問題:這裡面咱們猜想一下,Thread-A和Thread-B執行後,y一定等于0麼?

解答:如果線程Thread-A的操作(int x=0)happens-before線程Thread-B的操作(int y=x),那麼y=0,如果Thread-A和Thread-B不滿足happens-before原則,那麼y不一定等于0。

關于happens-before定義和規則,我這裡直接摘錄《深入了解Java虛拟機》

happens-before原則定義:

1.如果一個操作happens-before另一個操作,那麼第一個操作的執行結果将對第二個操作可見,而且第一個操作的執行順序排在第二個操作之前。

2.兩個操作之間存在happens-before關系,并不意味着一定要按照happens-before原則制定的順序來執行。如果重排序之後的執行結果與按照happens-before關系來執行的結果一緻,那麼這種重排序并不非法。

happens-before原則規則:

程式次序規則:一個線程内,按照代碼順序,書寫在前面的操作先行發生于書寫在後面的操作;

鎖定規則:一個unLock操作先行發生于後面對同一個鎖額lock操作;

volatile變量規則:對一個變量的寫操作先行發生于後面對這個變量的讀操作;

傳遞規則:如果操作A先行發生于操作B,而操作B又先行發生于操作C,則可以得出操作A先行發生于操作C;

線程啟動規則:Thread對象的start()方法先行發生于此線程的每個一個動作;

線程中斷規則:對線程interrupt()方法的調用先行發生于被中斷線程的代碼檢測到中斷事件的發生;

線程終結規則:線程中所有的操作都先行發生于線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的傳回值手段檢測到線程已經終止執行;

對象終結規則:一個對象的初始化完成先行發生于他的finalize()方法的開始

重排序

Java程式在執行的時候,為了提高自身性能并且遵循as-if-serial語義,處理器和編譯器會對指令進行重排序,不會對存在資料依賴關系的操作進行重排序,并且單線程環境下不能改變程式運作的結果才能進行重排序。

舉個例子:

int x=5; 
int y=8; 
int z=x+y;
           

關于上面的代碼,存在這樣關系:X和Z之間存在資料依賴關系,同時Y和Z之間也存在資料依賴關系。為了得到正确的結果,執行指令序列時,Z不能重排序到X和Y的前面。但是X和Y之間并沒有資料依賴關系,根據我們上面講的,沒有資料依賴關系處理器和編譯器會對指令進行重排序,是以X和Y的執行順序會被重新調整下面兩種情況:

Y-》X-》Z 結果為:13
X-》Y-》Z 結果為:13
           

上面的代碼如果是多線程的情況下進行重排序,會影響程式的運作結果,是以才引發出多線程高并發下資料不一緻一系列問題。這裡有引出了另一個概念記憶體屏障。

記憶體屏障

記憶體屏障的出現主要是禁止處理器的重排序,并強制把寫緩沖區中的髒資料寫回主記憶體,進而讓程式按我們預想的流程去執行。記憶體屏障有些材料又叫記憶體栅欄,是一個CPU指令,volatile就是基于記憶體屏障實作的。

記憶體屏障分Load Barrier(讀屏障)和Store Barrier(寫屏障)兩種。

對于讀屏障,在指令前插入Load Barrier,可以讓高速緩存中的資料失效,強制從主記憶體加載資料;

對于寫屏障,在指令後插入Store Barrier,可以讓寫入緩存中的最新資料更新寫入主記憶體,讓其他線程可見。

對于Load和Store實際又分為以下四種

LoadLoad屏障

序列: Load1;Loadload;Load2

說明: 確定Load1所要讀入的資料能夠在被Load2和後續的load指令通路前讀入。

StoreStore屏障

序列: Store1;StoreStore;Store2

說明: 確定Store1的資料在Store2以及後續Store指令操作相關資料之前對其它處理器可見。

LoadStore屏障

序列: Load1;LoadStore;Store2

說明: 確定Load1的資料在Store2和後續Store指令被重新整理之前讀取。

StoreLoad屏障

序列: Store1;StoreLoad;Load2

說明: 確定Store1的資料在被Load2和後續的Load指令讀取之前對其他處理器可見。

以上四種屏障詳情通路:

http://ifeve.com/jmm-cookbook-mb/

三.參考文獻

1.Java并發程式設計的藝術

2.深入了解Java虛拟機

3.揭秘Java虛拟機-JVM設計原理與實作