Java內存模型(JMM)
1.對內存模型的介紹
①對Java內存模型的結構圖
- java的線程之間的通訊是經過「共享內存」的方式進行隱式通訊,即線程A把某狀態寫入主內存中的共享變量X,線程B讀取X的值,這樣就完成了通訊。是一種隱式的通訊方式。
- 一個線程的模型能夠類比如今的CPU,一個CPU會具有高速緩存,來緩解CPU速度和內存IO速度的巨大差距,線程也是相似的,一個線程擁有其本地內存,至關因而用來緩存主內存中的值的。
- 也就是說,線程並不直接與主內存通訊,而是線程先把主內存中的共享變量備份到私有的本地內存中,線程是使用本地內存中的值。
- 虛擬機,甚至硬件自己的優化措施,會優先將本地內存存儲在寄存器和高速緩存。
②JMM的做用
java的最大賣點是「一次編寫,處處運行」,爲了實現讓java在各類平臺上都能有一致的內存訪問效果,java虛擬機規範必須定義一種Java內存模型來屏蔽各類硬件和操做系統的內存訪問差別。java
③JMM的特徵
java內存模型是圍繞着如何處理原子性,可見性,有序性來創建的。程序員
- 原子性:原子性指多個操做的組合要麼一塊兒執行完,要麼所有不執行,這個很好理解,一個線程在執行一組操做的中途,不能被另外一個線程插一腳,否則會形成數據錯誤,最經典就是 a++;操做,++ 操做符不是原子的,因此須要使用同步工具保證其原子性。
- 可見性:根據java內存模型的結構,各個線程都會從主內存備份一個變量的工做內存放在本身的工做內存做爲緩存,能夠提升效率,這樣就形成了可見性問題,即一個線程修改了一個數據,若是一沒有當即同步回主內存,二沒有讓其餘使用這個數據的線程及時從主內存同步,則其餘線程的數據是錯誤的。
- 有序性:編譯器和處理器爲了得到更高的效率,會對指令進行重排序,實際生成的字節碼指令順序或者處理器指令順序並不是是程序源代碼中的順序,這個在單線程的狀況下問題不大,由於編譯器和處理器會保證結果正確,可是多線程的環境下,由於線程之間不少時候須要協調,若是指令進行重排,會影響協調結果錯亂,能夠從一個經典的例子來講明,代碼以下:
假設有兩個線程A,B ,A線程先執行write方法,接下來B線程執行read()方法,write方法中的兩個操做,並無必要的順序關係,在實際執行中,編譯器或者處理器有權利進行重排序,先對flag賦值,而後對a賦值,巧了,B線程對flag的讀取正好在A線程兩個操做的中間,即B線程讀取到了flag爲true,可是a卻仍是0,形成了數據的錯誤。。
所以,JMM必須提供了一種機制來禁止相似的重排序,詳見volatile的內存語義,其提供了對有序性的保證。編程
class OrderExample{
int a =0 ;
boolean flag = false;
public void write(){
a = 1;
flag = true;
}
public void read(){
if(flag){
int i = a + 1;
}
}
}
④JMM的設計要求
咱們能夠把JMM看作是程序員和平臺的中間人。程序員和平臺須要談判卻==不直接交流==,而是經過JMM來傳話。
先看雙方的須要:緩存
- 程序員的需求:程序員但願內存模型簡單易懂,符合人類的直覺,因此渴望一個強內存模型
- 編譯器和處理器的須要:編譯器,處理器但願內存模型對本身的束縛越小越好,這樣就能夠作更多的優化來提升執行速度,編譯器,處理器渴望弱內存模型。
- 因此JMM有兩個設計需求,1.爲程序員提供可見性保證,2.儘量放鬆對編譯器,處理器的限制。
所謂談判,是一個妥協的過程,JMM給了程序員一些「先行發生原則happens-before」的保證,程序員的代碼中的操做之間關係只要符合這些規則,那麼平臺不會隨意對這些操做重排序,程序員根據這個保證,可使編程更加容易和健壯,更符合人類的直覺。同時,JMM也放寬了對平臺的限制,只要能保證那些「happens-before規則」,平臺能夠對操做進行重排序。安全
2.內存模型如何實現三個特性
①主內存與工做內存之間的交互協議
定義了一個變量如何從主內存中拷貝到工做內存(本地內存),如何從工做內存同步回主內存的實現細節。
JMM中定義了8中操做來完成以上工做,每種操做都是原子的,不可再分的(double,long類型的變量,load,store,read,write操做在某些平臺上容許有例外)多線程
命令 |
做用於何處的變量 |
做用描述 |
lock |
主內存 |
把一個變量標識爲一個線程獨佔的狀態 |
unlock |
主內存 |
把一個變量從鎖定狀態釋放出來,與lock對應 |
read |
主內存 |
把一個變量的值從主內存中傳輸到線程的工做內存,供load使用 |
load |
工做內存 |
把read操做獲得的值放入到工做內存的變量副本中 |
use |
工做內存 |
把工做內存中的值傳遞給執行引擎 |
assign |
工做內存 |
把從執行引擎收到的值賦值給工做內存中的該變量 |
store |
工做內存 |
將工做內存中的該變量的值傳送到主內存 |
write |
主內存 |
將store操做獲得的值放入主內存的變量中 |
這些操做須要必須遵循的規定:併發
- JMM規定read-load,store-write兩對操做必須順序執行,並且必須成對出現,可是不規定連續執行,
- 工做內存有狀態的改變必須同步會主內存
- 不容許沒有發生過任何assign的狀況下把數據同步回主內存
- 一個新的變量只能在主內存中誕生,即use和store以前必須有對該變量的assign,load
- 一個變量在同一時刻只容許一個線程對其執行lock操做,可是容許該線程屢次執行lock操做,對應的,unlock也必須執行相同的次數才能解鎖。可重入鎖
- 對一個變量執行lock操做,必須清空工做內存中此變量的值,在執行引擎使用該變量以前,從新執行load或assign。 synchronized也具有內存可見性
- 若是一個變量事先未被Lock鎖定,那麼不容許對其unlock操做,也不能unlock一個被其餘線程鎖定的變量。
- 對一個變量unlock操做以前,必須把此變量同步會主內存。也可服務於synchronized的可見性
8中內存訪問以及上述的8個規定限制,加上volatile的寫特殊規定,已經徹底肯定了java程序的那些內存訪問操做是線程安全的。
以上的規定的一個等效判斷原則就是 ==happens-before==。
②三個特性的實現
-
原子性的實現,原子性指不可分割的操做
- JMM使用read,load,assign,use,store,write來訪問基本數據,這些操做都是原子的,因此基本能夠認爲JMM對基本數據類型的訪問是原子的
- 對於更大範圍的原子性保證,JMM提供了lock和unlock來知足這種需求,lock和unlock並未直接提供給用戶使用,可是能夠經過更高級的字節碼指令,monitorenter,monitorexit來隱式使用。JVM基於進入和退出Monitor對象來實現方法同步和代碼塊同步,具體的使用能夠看synchronized原語的實現。
-
可見性:當一個線程修改了共享變量的值,對其餘線程當即可見。
- volatile變量提供的可見性:普通變量和volatile變量都是經過主內存做爲線程間分享數據的渠道,不一樣的是,volatile變量能及時同步到主內存而且其餘線程工做內存中該變量的值當即失效,須要從新從主內存中加載,普通變量不保證。
- synchronized提供的可見性:實現方式和volatile有所不一樣,"八大規定"中說,unlock操做以前,必須先把變量同步到主內存中,而lock以前,必須清空工做內存中的值,從新從主內存中加載。這兩條保證了synchronized具有內存可見性。
- final提供的內存可見性:final的重排序規則明確了,只要正確構造一個對象,那麼當線程得到這個對象的時候,其final域已經正確完成初始化,對其餘線程可見。
-
有序性:針對指令重排,java提供了volatile和synchronized,可是實現方式是不一樣的
- volatile自己禁止指令重排序,
- synchronized像是把多線程的環境變爲了單線程的環境,並行變串行,指令重排必須保證串行語義的一致性。
③「自然的」先行發生原則 happens-before
happens-before服務於三大原則中的有序性,Java程序中自然的(未使用volatile或者synchronized)有序性能夠總結爲一句話:app
若是在本線程觀察,全部操做都是有序的,若是在另外一個線程中觀察,全部操做都是無序的。
試想一下,若是編碼過程當中全部的操做都要使用volatile或者synchronized來保證有序性,那麼將是多大的負擔,程序的複雜性也會極大上升。因此java中提供了一些自然的先行發生原則,是指那些無需任何同步手段就自然具有順序性的先行發生原則。是8個內存操做的規則的另外一種表達。工具
- 程序次序規則:在一個線程內,按照控制流,前面的操做先於後面的操做,對後續操做可見。as-if-serial語義。
- 管程鎖定規則:一個unlock操做必須先行發生於同一個鎖的lock操做
- volatile變量規則:對volatile的寫操做必須happens-before後續對該變量的讀操做。
- 線程啓動規則:start()方法happens-before該線程的其餘全部動做
- 線程終止規則:線程中全部的操做happens-before該線程的終止檢測,如isAlive()方法。
- 線程中斷規則:interrupt()方法happens-before對線程中斷的檢測
- 對象終結規則:一個對象初始化完成happens-before其finilize()方法的開始
- 傳遞性,A hannens-before B,B happens-before C,則A happens-before C.
若是兩個操做的關係沒法從上述規則中推倒出來,則虛擬機能夠對它們隨意重排序。衡量併發安全問題,應該以先行發生原則爲準。優化