Java內存模型

Java內存模型

  爲了屏蔽各類硬件和操做系統的內存訪問差別,實現Java在不一樣平臺下都能達到一致的內存訪問效果,而定義出的一種內存模型規範。java

1、主內存和工做內存

  Java內存模型的主要目標是爲了定義程序中各個變量的訪問規則(虛擬機中讀寫變量....這些變量包括實例字段、靜態字段、構成數組對象的元素,但不包括線程私有而不存在競爭的方法參數和局部變量)。Java內存模型並無如今執行引擎使用處理器的特定寄存器或者緩存來和主內存進行交互,也沒有限制編譯器調整代碼順序執行這一類優化措施。數組

  Java內存模型規定全部變量都存儲在主內存中,除此以外,每條線程擁有本身的工做內存(線程的工做內存中保存的是執行時候須要使用的變量從主內存中拷貝的副本),且線程執行的時候對變量的讀寫操做都是在本身的工做內存中進行,而不是直接從主存中讀或者寫。(不一樣的線程之間不能訪問彼此的工做內存,線程之間的訪問通訊均須要經過主內存來完成)緩存

  

2、內存建交互操做

  一、上面區分了主內存和工做內存兩者的關係和區別,那麼實際上JMM中定義了下面幾種操做來完成變量從主內存拷貝到工做內存、而後從工做內存同步寫回主內存中。並且這些操做都是原子性的(64位的double類型和龍類型可能會被拆分紅32位來進行操做)安全

  ①lock(鎖定):做用與主內存中的變量,將一個變量標識爲線程獨佔的狀態;併發

  ②unlock(解鎖):做用於主內存中的變量,將一些被線程獨佔鎖定的變量釋放,從而能夠被其餘線程佔有使用;app

  ③read(讀取):做用於主內存中的變量,將變量的值從主內存中傳輸到工做內存中,以便後去的load操做使用;ide

  ④load(載入):做用於工做內存中的變量,將上面read操做從主內存中獲取的值放在本身的工做內存中的變量副本中;函數

  ⑤use(使用):做用於工做內存中的變量,將工做內存中的變量值傳遞給執行引擎,當虛擬機須要使用變量的值的時候回執行這個操做;優化

  ⑥assign(賦值):做用於工做內存中的變量,將一個從執行引擎接受到的值賦給工做內存中的該變量,當虛擬機遇到給變量賦值操做的指令時候執行;this

  ⑦store(存儲):做用域工做內存中的變量,將工做內存中的變量值傳送回主內存中,以便後續的write操做使用;

  ⑧write(寫入):做用於主內存中的變量,將store操做從工做內存中獲得的變量的值寫回主內存的變量中。

  二、對於一個變量而言:若是要從主內存複製到工做內存,就須要順序執行read和load操做;若是須要將其寫回主內存,就須要順序的執行store和write操做。(這兩種狀況是須要按序執行的,可是不限制必須連續執行,在他們操做之間能夠執行其餘指令)

  三、一些其餘的規則

  ①不容許read和load、store和write操做中的一個單獨出現(即不容許將一個變量從主內存中讀取可是工做內存不接受、或者是從工做內存寫回可是主內存不接受的狀況);

  ②不容許一個線程丟棄最近使用的assign操做(即變量在工做內存中改變了以後須要將變化同步回主內存之中);

  ③不容許一個線程在沒有發生assign操做的時候,就將數據從工做內存同步回主內存之中;

  ④一個新的變量只能在主內存之中產生,不容許在工做內存中直接使用一個未被初始化的變量(對這個變量執行load或assign操做),即對一個變量執行use和store操做以前必須執行了assign和load操做;

  ⑤若是對一個變量進行lock操做,將會清空工做內存中該變量的值,在執行引擎使用這個變量以前,須要從新執行load和assign操做;

  ⑥若是一個變量事先沒有被lock操做鎖定,那麼不容許對其執行unlock操做,也不容許某一個線程去unlock一個被其餘線程lock住的變量;

  ⑦在對一個變量執行unlock以前,必須將這個變量的值同步回主內存中。

3、volatile變量

  一、volatile是JVM提供的輕量級同步機制 

  當一個變量被定義爲volatile以後,會具有下面兩種特性

  a)保證此變量對於其餘全部線程的可見性(當某條線程改變了這個volatile的值後,其餘的線程可以得知這個變化)。這裏須要指出:

  ①volatile變量存在不一致的狀況(雖然各個線程中是一致的,可是這種狀況的緣由是在使用變量以前都須要刷新,致使執行引擎看不到不一致的狀況,那麼在各個線程中看到的天然就是一致的);

  ②Java中的運算不是原子的,致使基於volatile變量在併發狀況下的操做不必定就是安全的,如同下面的例子:使用10個線程對volatile類型的變量count進行自增運算操做,而後觀察運行的計算結果發現並非指望的100000,而是小於該值的某個其餘值

 1 package cn.test.Volatile;
 2 
 3 import java.util.ArrayList;
 4 import java.util.List;
 5 
 6 public class TestVolatile02 {
 7     volatile int count = 0;
 8     void m(){
 9         count++;
10     }
11 
12     public static void main(String[] args) {
13         final TestVolatile02 t = new TestVolatile02();
14         List<Thread> threads = new ArrayList<>();
15         for(int i = 0; i < 10; i++){
16             threads.add(new Thread(new Runnable() {
17                 @Override
18                 public void run() {
19                     for(int i = 0; i < 10000; i++){
20                         t.m();
21                     }
22                 }
23             }));
24         }
25         for(Thread thread : threads){
26             thread.start();
27         }
28         for(Thread thread : threads){
29             try {
30                 thread.join();
31             } catch (InterruptedException e) {
32                 // TODO Auto-generated catch block
33                 e.printStackTrace();
34             }
35         }
36         System.out.println(t.count);
37     }
38 }

 

  ③實際上使用javap反編譯以後的代碼清單,咱們查看m()方法的彙編代碼,發現count++實際上有四步操做:getfield->iconst_1->iadd->putfield,分析上面程序執行和預期結果不一樣的緣由:getfield指令把count值取到棧頂的時候,volatile保證了count的值在此時是正確的,可是在執行iconst_1和iadd的時候,其餘線程可能已經將count的值增大了,這樣的話剛剛保存在操做數棧頂的值就是過時的數據了,因此最後putfield指令執行後就可能把較小的值同步到主存當中了。

  void m();
    Code:
       0: aload_0
       1: dup
       2: getfield      #2                  // Field count:I
       5: iconst_1
       6: iadd
       7: putfield      #2                  // Field count:I
      10: return

 

  ④上面的代碼要想保證執行正確,咱們還須要在執行m方法的時候使用鎖的機制來保證併發執行的正確性,可使用synchronized或者併發包下面的原子類型。下面須要使用這兩種加鎖機制的場合

  □運算結果不依賴變量的當前值,或者可以確保只有單一的線程修改變量的值

  □變量不須要與其餘的狀態變量共同參與不變約束。

  ⑤看下面的代碼,就比較適合volatile。使用volatile類型的變量b可以在其餘線程調用endTest修改b的值以後,全部doTest的線程都可以停下來。

 1 package cn.test.Volatile;
 2 
 3 import java.util.concurrent.TimeUnit;
 4 
 5 public class TestVolatile01 {
 6     volatile boolean b = true;
 7 
 8     void doTest(){
 9         System.out.println("start");
10         while(b){}
11         System.out.println("end");
12     }
13 
14     void endTest() {
15         b = false;
16     }
17 
18     public static void main(String[] args) {
19         final TestVolatile01 t = new TestVolatile01();
20         new Thread(new Runnable() {
21             @Override
22             public void run() {
23                 t.doTest();
24             }
25         }).start();
26 
27         try {
28             TimeUnit.SECONDS.sleep(1);
29         } catch (InterruptedException e) {
30             e.printStackTrace();
31         }
32         t.endTest();
33     }
34 }

 

  b)禁止指令重排序優化

  普通變量只能保證在程序執行過程當中全部依賴賦值結果的地方都能獲取到正確的結果,可是不能保證變量賦值操做的順序與程序中的代碼執行順序一致。而指令的重排序就可能致使併發錯誤的結果。使用Volatile修飾就能夠避免由於指令重排序致使的錯誤產生。

  c)java內存模型中對volatile變量定義的特殊規則。假定T表示一個線程,V和W分別表示volatile型變量,那麼在進行read、load、use、assign、store和write操做時須要知足以下規則:

  ①只有當線程T對變量V執行的前一個動做爲load時,T才能對V執行use;而且,只有T對V執行的後一個動做爲use時,T才能對V執行load。T對V的use,能夠認爲是和T對V的load。read動做相關聯,必須連續一塊兒出現(這條規則要求在工做內存中,每次使用V前都必須先從主內存刷新最新的值,用於保證能看見其餘線程對V修改後的值)。

  ②只有當T對V的前一個動做是assign時,T才能對V執行store;而且,只有當T對V執行的後一個動做是store時,T才能對V執行assign。T對V的assign能夠認爲和T對V的store、write相關聯,必須連續一塊兒出現(這條規則要求在工做內存中,每次修改V後都必須馬上同步回主內存中,用於保證其餘線程看到本身對V的修改)。

  ③假定動做A是T對V實施的use或assign動做,假定動做F是和動做A相關聯的load或store動做,假定動做P是和動做F相應的對V的read或write動做;相似的,假定動做B是T對W實施的use或assign動做,假定動做G是和動做B相關聯的load或store動做,假定動做Q是和動做G相應的對W的read或write動做。若是A先於B,那麼P先於Q(這條規則要求volatile修飾的變量不會被指令的重排序優化,保證代碼的執行順序與程序的順序相同)。

4、原子性、可見性與有序性

  一、原子性:由Java內存模型來直接保證的原子性變量操做(包括read,load,assign,use,store,write),能夠認爲基本數據類型的訪問讀寫都是具有原子性的(儘管double和long的讀寫操做劃分爲兩次32位的操做來執行,可是目前商用虛擬機都將64位的數據讀寫操做做爲原子性來對待)

  二、可見性:當一個線程修改了共享變量的值,其餘線程可以當即獲得這個修改的情況。除了volatile,Java還有兩個關鍵字能實現可見性,synchronized和final。同步塊的可見性是由「對一個變量執行unlock操做以前,必須把此變量同步回主內存中(執行store和write操做)」這條規則得到的,而final關鍵字的可見性是指:被final修飾的字段在構造器中一旦被初始化完成,而且構造器沒有把「this」的引用傳遞出去(this引用逃逸是一件很危險的事情,其餘線程有可能經過這個引用訪問到「初始化了一半」的對象),那麼其餘線程中就能看見final字段的值。

  三、有序性:Java提供了volatile和synchronized兩個關鍵字來保證線程之間操做的有序性,volatile關鍵字自己就包含了禁止指令重排序的語義,而synchronized則是由「一個變量在同一時刻只容許一條線程對其進行lock操做」這條規則得到的,這個規則決定了持有同一個鎖的兩個同步塊只能串行地進入。

5、先行發生原則(happens-before)

  一、先行發生原則:是Java內存模型中定義的兩項操做之間的偏序關係,若是說操做A發生在操做B以前,那麼操做A產生的結果能被操做B觀察到(這個結果包括修改內存中共享變量的值、發送通訊消息、調用某個方法等等)。

  二、下面是Java內存模型中的一些先行發生關係,這些happens-before關係不須要任何的同步操做就已經存在,能夠在編碼中直接使用,若是兩個操做之間的關係不在此列,而且沒法從下列規則中推導出來的話,他們就沒有順序性保證,那麼虛擬機就能夠對其進行重排序優化。

  ①程序次序規則:在一個線程內,按照程序控制流(包括分支、循環)順序執行。

  ②管程鎖定規則:一個unlock操做先行發生於後面對同一個鎖的lock操做。

  ③volatile變量規則:對一個volatile變量的寫操做先行發生於後面對這個變量的讀操做。

  ④線程啓動規則:Thread對象的start方法先行發生於此線程的每一個動做。

  ⑤線程終止規則:線程中的全部操做都先行發生於此線程的終止檢測,咱們能夠經過Thread.join()方法結束/Thread.isAlive()的返回值等手段檢測到線程已經終止執行。

  ⑥線程終端規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,能夠經過Thread.interrupted()方法檢測到是否有中斷髮生。

  ⑦對象終結規則:一個對象的初始化完成(構造函數執行結束)先行發生於他的finalize方法的開始、

  ⑧傳遞性:若是操做A先行發生於操做B,操做B先行發生於操做C,那麼操做A先行發生於操做C。

相關文章
相關標籤/搜索