簡單定義
JMM定義了java 虛擬機(JVM)在計算機內存(RAM)中的工作方式。JVM是整個計算機虛擬模型,所以JMM是隸屬于JVM的 Java內存模型定義了多線程之間共享變量的可見性以及如何在需要的時候對共享變量進行同步。原始的Java內存模型效率并不是很理想,因此Java1.5版本對其進行了重構,現在的Java8仍沿用了Java1.5的版本 主內存是線程是共享區域,每個線程都有自己的工作內存為何要有內存模型
//我們來看這個例子public class TestA { static int a = 0, b = 0; //定義PRivate static a,b為0,public static void main(String[] args) throws InterruptedException { while(true){ Thread threadA = new Thread(new Runnable() { public void run() { a = 2; //write 2 -> a b = 1; //write 1 -> b } }); //線程1 Thread threadB = new Thread(new Runnable() { public void run() { a = 200;//write 200 -> a b = 100;//write 100 -> b } }); //線程2 threadA.start(); threadB.start(); threadA.join(); threadB.join(); //打印a,b System.out.println("a="+a+",b="+b); } } }}想想看上面的結果會是多少? a=2,b=1? a=200,b=100? a=2,b=100? a=200,b=1? 為什么會有這種問題? 我們在代碼尾部加上
if( !((a==2 && b==1) || (a==200 && b==100)) ){ System.out.println("a="+a+",b="+b +" >>>> i="+i); break; }輸出結果: a=2,b=100 >>>> i=128348 [Finished in 42.9s]
因為ab為共享數據而兩個線程在不斷的對它進行讀寫,沒有次序的,沒有規則的,因此會導致數據問題,那么在內存模型中數據時如何交互的呢?
數據交互
例如: 我們定義了“晚餐是魚香肉絲蓋飯”在主內存中 線程B拿到這個數據保存到數據副本中 線程B將晚餐改為“晚餐是蔥油拌面” 當線程C在18:00時,拿到主內存中的晚餐,得到的數據就是“蔥油伴面” 從而完成了線程通信 當然你也許想到了 主內存數據同步問題。線程不安全問題,不要著急下面會說到這些
線程棧與堆
當前線程所執行方法的調用信息,2個線程棧并不相互可見。 所有基本數據類型都在線程棧中保存 (boolean/char/byte/short/int/long/float/double) heap(堆)包含了Obecjt對象,無論它是哪個線程創建的都放在了heap中 如果線程棧中包含一個對象引用那么,引用將會存在stack中而對象本身依然在heap中 static修飾的變量,類都存在heap中同步與可見性
對于上面代碼中例子的非線程安全,是因為我們沒有使用volatile和synchronized進行修飾,導致共享對象的可見性,線程安全性無法得到保證,數據異常
【可見性】例子:
主存包含 i = 0 ThreadA -> read -> 主存 -> i ThreadA -> copy -> i -> 數據副本 ThreadA -> write -> i = 999 -> 數據副本 ThreadB -> read -> 主存 -> i ThreadB -> copy -> i -> 數據副本 ThreadA -> write -> 數據副本i -> 主存
ThreadB獲取的 i依然為0,因為ThreadA 還沒有將數據副本寫回主存,ThreadA是在ThreadB之后寫回的數據
最終問題在于ThraedB與ThreadA沒有可見性,ThreadB 并不 happens before ThreadA
如果我們使用volatile關鍵字就不會發生這種狀況,可以保證數據直接從主存rw,當然其中原理是基于內存屏障指令來達到的(volatile只能保證可見性,但無法保證線程安全)【同步】例子
主存包含 i = 0 ThreadA -> read -> 主存 -> i ThreadB -> read -> 主存 -> i
ThreadA -> copy -> i -> 數據副本 ThreadB -> copy -> i -> 數據副本
ThreadA -> write -> i = i + 1 -> 數據副本 ThreadB -> write -> i =i +1 -> 數據副本
從此刻來看 i 的值應該是 2 ,其實不然,如果是串行執行,自然是 i+1 , i+1 ,但多線程中往往是并行的,那么無論是ThreadB還是A,最終主存中的值只會是2如果我們使用sync(synchronized)關鍵詞即可保證只能有一個線程在操作目標對象,避免線程安全問題,同時sync也保證數據的可見性(如同volatile),則數據也是從主存中直接r/w
synchronized 同步,可見性
volatile 非線程安全,可見性指令重排 為了提高程序運行性能,jvm和cpu會對代碼進行重新排序 然而內存屏障指令會禁止在特定類型修飾的目標(volition,sync)中進行指令重排,以保證可見性
編譯器優化重排序:編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序。 指令級并行的重排序:如果不存l在數據依賴性,處理器可以改變語句對應機器指令的執行順序。 內存系統的重排序:處理器使用緩存和讀寫緩沖區,這使得加載和存儲操作看上去可能是在亂序執行。舉個例子:
int a=1;int b=2;int c=3; Systen.out.print(a+c);可以發現int b =2 與print并沒有依賴關系并不影響 as-if-serial原則,所以int b是可以被重排的,然而 a,c的定義也是可以重排的 但是他們一定會在print前面。
as-if-serial 原則
該原則定義了在指令重排中,無論如何重排,都不可以改變原有結果
Happens Before
從jdk5開始,java使用新的JSR-133內存模型,基于happens-before的概念來闡述操作之間的內存可見性。在JMM中,如果一個操作的執行結果需要對另一個操作可見,那么這兩個操作之間必須要存在happens-before關系,這個的兩個操作既可以在同一個線程,也可以在不同的兩個線程中。與程序員密切相關的happens-before規則如下:1.程序順序規則:一個線程中的每個操作,happens-before于該線程中任意的后續操作。2.監視器鎖規則:對一個鎖的解鎖操作,happens-before于隨后對這個鎖的加鎖操作。3.volatile域規則:對一個volatile域的寫操作,happens-before于任意線程后續對這個volatile域的讀。(內存屏障flush)4.傳遞性規則:如果 A happens-before B,且 B happens-before C,那么A happens-before C。注意:兩個操作之間具有happens-before關系,并不意味前一個操作必須要在后一個操作之前執行!僅僅要求前一個操作的執行結果,對于后一個操作是可見的假定我們有已經被初始化的變量: int counter = 0; 這個 counter 變量被兩個線程所共有,也就是說線程A和線程B都可以獲取或者更改counter的值。 這里我們假設線程A要增加counter的值: counter++; 然后,線程B打印counter的值 System.out.println(counter); 如果上面兩條語句被同一個線程執行,我們可以肯定的說打印出來的值是1. 但是如果這兩條語句分別被兩個線程執行,其打印出來的值卻可能是0, 因為這里并沒有任何保證說線程A對counter的修改一定對線程B所見。除非我們在兩條語句之間建立起 happens-before的關系
參考資料: 《深入理解Java內存模型》 《全面理解Java內存模型》 Java多線程之happens-before
新聞熱點
疑難解答