什么叫Java內存模型?
現代計算機通過指令的重排序來提升計算機的性能,而沒有限制條件的指令重排序會使得程序的行為不可預測,JMM就是通過一系列的操作規則限制指令重排序的方式使得指令重排序不會破壞JMM提供的可見性,同時JMM通過讓JVM在適當的位置插入內存柵欄來屏蔽JMM與底層平台內存模型之間的差異。
背景知識:
*每秒處理事務數:衡量一個服務性能的高低好壞,每秒處理事務數是重要的衡量指標之一
*高速Cache:由於計算機的存儲設備與處理器的運算速度有幾個數量級的差距,所以現代計算機都不得不加入一層讀寫速度盡可能的接近處理器運算速度的高速緩存來作為內存和處理器直接的緩沖
*緩存一致性協議:用於處理給高速緩存中數據一致性的問題
*處理器<--->高速緩存<--->緩存一致性協議<--->主內存
*如果存在一個計算任務依賴另外一個計算任務中間結果,那么其順序性並不能依靠代碼的先后來保證,這是數據的依賴性,指令重排優化要遵循數據的依賴性
*Java線程<--->工作內存<--->sava和store操作<--->主內存
ps:放圖的目的就是要類比上面兩張圖!!!
正文:
對比上面兩張圖,我們把處理器和java線程做類比,高速緩存和工作內存做類比,主內存是同一個,那么我們java中有沒有類似緩存一致性協議的協議來處理工作內存和主內存之間的實現細節呢?即一個變量如何從主內存拷貝到工作內存,如何從工作內存同步回到主內存的呢
答案肯定是有的,java內存模型中(和java內存區域中的堆,棧,方法區等不是同一個層次的划分)定義了8種操作來實現主內存和工作內存之間的交互協議,每一種操作都是原子性的!
java內存模型中的8種操作:
1)lock:鎖定,作用於主內存變量,把一個變量標識為一條線程獨占狀態
2)unlock:解鎖,作用於主內存變量,把一個處於lock狀態的變量釋放出來,釋放后的變量才可以被其他線程鎖定
3)read:讀取,作用於主內存的變量,把一個變量從主內存傳輸到線程工作內存,以便隨后的load使用
4)load:載入,作用於工作內存變量,它把read操作從主內存中得到的值放入工作內存的變量副本中
5)use:使用,作用於工作內存變量,把工作內存變量的值傳遞給執行引擎,每當虛擬機遇到一個需要使用變量的值的字節碼指令就會執行這個操作
6)assign:賦值,作用於工作內存變量,把從執行引擎接收到的值賦給工作內存變量,每當虛擬機遇到一個需要給變量賦值的字節碼指令就會執行這個操作
7)store:存儲,作用於工作內存變量,把一個工作內存變量的值送到主內存中,以便后面的write操作使用
8)write:寫入,作用於主內存變量,把store操作從工作內存得到的變量的值放入主內存變量中
以上8個操作都是原子操作!
要從主內存讀取一個數據,必定要執行read和load,而且read要在load前面執行,只是要求了執行的順序,卻沒有要求一定要連續執行,所以我們可以進行指令重排,那我們這8種操作怎么保證多線程環境下是安全的呢?所以我們java中還存在8種操作的操作規則
操作規則:
1)read在load前,store在write前,都不能單獨出現,得成對,出現還得滿足順序
2)工作內存中的變量改變后必須同步到主內存
3)不允許一個線程無原因的(沒有發生任何assign操作)把數據從線程的工作內存同步到主內存中
4)新的變量只能在主內存中誕生,不允許在工作內存中使用一個沒有被初始化(load和assign)的變量,即對一個變量實施use和store操作之前必須先執行過了assign和load
5)一個變量在同一時刻只允許一個線程對其進行lock,但lock操作可以被同一條線程執行多次,執行多次lock后只有進行相同次數的unlock才能釋放變量
6)如果對一個變量執行lock操作,那將會清空工作內存中此變量的值,在執行引擎使用這個變量前,需要重新執行load或assign操作來初始化變量的值
7)如果一個變量沒有被lock操作鎖定,那么不允許對它進行unlock操作,也不允許去unlock一個被其他線程lock的變量
8)對一個變量執行unlock操作之前,必須把此變量同步回到主內存中(執行store,write)
分析:
這8種操作加上這8種操作規則,雖然可以保證一些內存操作是安全的,但是它實現起來非常的繁瑣,不好實現,我們有一種代替這8個規則的方法:先行發生原則,滿足先行發生原則則可以保證這些內存在多線程環境下是安全的,如果不滿足先行發生原則的話,我們的補救措施就是volatile和synchorized
先行發生原則(判斷數據是否存在競爭,線程是否安全的重要依據):
天然的先行發生關系,這些先行發生關系無需同步就已經存在了,滿足這些先行發生關系的話,我們就不可以對他們進行隨意的指令重排,得設置內存屏障,防止被重排
具體的原則:
1)程序次序規則:在一個線程內,按照程序代碼順序,書寫在前面的操作先行發生於書寫在后面的操作,准確的說,應該是控制流順序,而不是程序代碼順序,因為要考慮分支循環等結構
2)管程鎖定原則:一個unlock操作先行發生於后面對同一個鎖的lock操作,這里必須強調是同一個鎖,后面是指時間上的先后順序
3)volatile變量規則:對一個volatile變量的寫操作要先行發生於后面對這個變量的讀操作,這里的后面同樣是指時間上的先后順序
4)線程啟動規則:Thread對象的start方法先行發生於此線程的每一個動作
5)線程終止原則:線程中的所有操作都優先發生於對此線程的終止檢測,我們可以通過Thread.join方法,Thread.isAlive的返回值等手段檢測到線程已經終止執行
6)線程中斷規則:對線程interrupu方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,可以通過Thread.interrupted方法檢測到是否有中斷發生
7)對象終結規則:一個對象的初始化完成(構造函數執行結束)先行發生於它的finalize方法
8)傳遞性:如果操作A先行發生於操作B,操作B先行發生於操作A,那么可以得出操作A先行發生於操作C的結論
分析:
兩個操作如果不滿足先行發生原則,那么這兩個操作在並發環境下就是不安全的,需要采用volatile或者synchorized或者lock使得線程安全,如果他們滿足先行發生原則,那么這兩個操作在多線程環境下肯定是線程安全的
(當然,volatile在java里面的運算的非原子性的,導致volatile變量的運算在並發下也一樣是不安全的,但是單個volatile變量的讀寫具有原子性!!!)
volatile變量規則:
關鍵字volatile是JVM中最輕量的同步機制,volatile具有兩種特性:
*保證變量的可見性:對一個volatile變量的讀,總是能看到(任意線程)對這個1volatile變量最后的寫入,這個新值對於其他線程來說是立即可見的
*屏蔽指令重排序:指令重排序是編譯器和處理器為了高效對程序優化進行的手段,下文有詳細分析:
volatile語義並不能保證變量的原子性,對任意單個volatile變量的讀寫具有原子性,但類似於i++,i--這種復合操作不具有原子性,因為自增運算包括讀取i,i+1,重新賦值三個步驟,並不具備原子性
由於volatile只能保證變量的可見性和屏蔽指令重新排序,只有滿足下面兩條規則時,才能使用volatile來保證並發安全,否則就需要加鎖(synchorized,lock,Atomic原子類)來保證並發中的原子性
*運算結果不存在數據依賴,或者只有單一的線程改變變量的值
*變量不需要與其他狀態變量共同參與不變約束
因為需要在本地代碼中插入許多內存屏蔽指令在屏蔽特定條件下重新排序,volatile變量的寫操作比讀操作慢一些,但是其性能開銷比鎖低很多