天天看點

JAVA如何解決可見性和有序性--java記憶體模型(Happens-Before規則,volatile,synchronized,final)

java記憶體模型

      • 這裡說的是JAVA如何解決其中的可見性和有序性問題。
        • Happens-Before規則
        • volatile關鍵字
        • final關鍵字
        • synchronized關鍵字

大家都知道java并發的三大根源性問題:可見性,有序性,原子性。那麼java是如何解決的呢?

這裡說的是JAVA如何解決其中的可見性和有序性問題。

導緻可見性的原因是緩存,導緻有序性的原因是編譯優化。那麼我們隻要按需禁用緩存和編譯優化就可以了。

JAVA推出了JAVA記憶體模型,JAVA記憶體模型規範了JVM提供按需禁用緩存和編譯優化的方法。具體來說,就是volatile,synchronized,final三個關鍵字,以及Happens-Before規則。

Happens-Before規則

happens-before僅僅要求前一個操作的執行結果對後一個操作是可見的,且前一個操作按順序排在後一個操作之前。
           

Happens-Before 限制了編譯器的優化行為,雖允許編譯器優化,但是要求編譯器優化後一定遵守 Happens-Before 規則,下面我來介紹這8大規則:A操作happens-before于B操作 == A happens(發生) B before(之前)

  1. 程式順序規則:一個線程中的每個操作,happens-before于該線程中任意後續操作。
  2. 螢幕鎖規則:對于一個鎖的解鎖,happens-before于随後對于這個鎖的加鎖。
  3. volatie變量規則:對于一個volatile變量的寫操作,happens-before于後續對該變量的讀操作。
  4. 傳遞性規則:如果Ahappen-beford B,且B happen-before C,那麼A happen-before C;
  5. 線程start()規則:main主線程啟動子線程B後,子線程B能夠看到main線程啟動子線程B之前的操作。
  6. 線程join()規則:線程A調用線程B的join()方法,線程A等待線程B執行完join()成功傳回後,線程B的任意操作都隊線程A中B.join()之後可見。
  7. 線程中斷規則:對線程interrupt()方法的調用先行發生于被中斷線程代碼檢測到中斷事件的發生,可以通過Thread.interrupted()檢測到是否發生中斷。
  8. 對象終結規則:這個也簡單的,就是一個對象的初始化的完成,也就是構造函數執行的結束一定 happens-before它的finalize()方法。

下面分别說說volatile,synchronized,final三個關鍵字。

volatile關鍵字

volatile可以保證可見性和有序性。

  • volatile如何保證有序性

重排序分為編譯器重排序和處理器重排序,為了實作volatile的記憶體語義,JMM為了限制這兩種重排序,對以下情況不能重排序!

1.當第一個操作是volatile讀時,不管第二個操作是什麼,都不能重排序;
2.當第二個操作是volatile寫時,不管第一個操作是什麼,都不能重排序;
3.當第一個操作是volatile寫,第二個操作是volatile讀,不能重排序;
           

為了實作以上規則(volatile的記憶體語義),編譯器在生成位元組碼時,會在指令序列中插入記憶體屏障禁止處理器重排序。但是對于編譯器,發現一個最優布置最小化插入記憶體屏障的數量是不可能的,是以JMM就在每個volatile讀寫前後都分别插入不同的記憶體屏障來實作有序性。

在每個volatile寫操作的前面插入一個StoreStore屏障。
在每個volatile寫操作的前面插入一個StoreLoad屏障。
在每個volatile讀操作的前面插入一個LoadLoad屏障。
在每個volatile讀操作的前面插入一個LoadStore屏障。
           

如果想了解四種屏障的作用,可以自己查閱資料。

  • volatile 如何保證可見性

有volatile修飾的共享變量在進行寫操作時,會多出lock的彙編代碼,而lock字首的指令在多核處理器下會進行兩個操作

1.将目前緩存行中的資料寫回到系統記憶體中。
LOCK#信号會鎖定這塊記憶體區域的緩存,并寫回記憶體,并使用緩存一緻性,保證它的原子性。
2.這個寫回記憶體的操作會使其他cup裡緩存了該記憶體位址的緩存行無效。
           

詳情參考volatile原理

final關鍵字

final域的重排序規則,編譯器和處理器要遵循下面兩個規則:

首先用java代碼展示這兩種情況。

public class FinalDemo{
int i;//普通變量
final int j;//final變量
static FinalDemo demo;
public FinalDemo(){//構造函數
	i=1;//寫普通域
	j=2;//寫final域
	}
public static void writer(){//寫線程A執行
demo=new FinalDemo();
}
public static void reader(){//讀線程B執行
FilalDemo object=demo;//讀對象引用
int a=object.i;//讀普通域
int b=object.j;//讀final域
}
}
           
  1. A在構造函數中對final域的寫入,與B随後把這個 被構造的對象的引用 給一個引用變量,這兩個操作不能重排序。
    JAVA如何解決可見性和有序性--java記憶體模型(Happens-Before規則,volatile,synchronized,final)
  2. 初次讀一個包含final域的對象的引用,與随後初次讀這個final域,這兩個操作不能重排序。
    JAVA如何解決可見性和有序性--java記憶體模型(Happens-Before規則,volatile,synchronized,final)

    上面我們看到的final域是基礎資料類型,那麼如果final域是引用類型呢?

    對于引用類型,寫final域的重排序規則對編譯器和處理器做了如下限制。

  3. 在構造函數中對一個final引用對象的成員域的寫入,與随後在構造函數外讀這個final域引用對象的成員域,這兩個操作不能發生重排序。

    請看下面執行個體代碼。

public class FinalReferenceDemo {
	final int[] arrays; //final是引用類型
	static FinalReferenceDemo demo;
	public FinalReferenceDemo() {//構造函數
		arrays = new int[1];//1
		arrays[0]=1;//2
	}
	public static void writeOne(){//寫線程A執行
		demo=new FinalReferenceDemo();//3
	}
	public static void writeTwo(){//寫線程B執行
		demo.arrays[0]=2;//4
	}
	public static void reader(){//讀線程C執行
		if(demo!=null){
			int temp=demo.arrays[0];
		}
	}
}

           
JAVA如何解決可見性和有序性--java記憶體模型(Happens-Before規則,volatile,synchronized,final)
  1. 構造函數溢出問題

    看如下代碼

public class FinalReferenceEscapeDemo {
	final int i;
	static FinalReferenceEscapeDemo obj;
	public FinalReferenceEscapeDemo() {// 構造函數
		i = 1;
		obj = this;
	}
	public static void writer() {
		new FinalReferenceEscapeDemo();
	}
	public static void reader() {// 讀線程C執行
		if (obj != null) {
			int temp = obj.i;
		}
	}
}
           
JAVA如何解決可見性和有序性--java記憶體模型(Happens-Before規則,volatile,synchronized,final)

如果上面這個例子太複雜,你可以結合下面這個例子再了解下

我們通常new一個對象時,有三步

我們想象的是這樣的

  1. 配置設定一塊記憶體 M;
  2. 在記憶體 M 上初始化 Singleton 對象;
  3. 然後 M 的位址指派給 instance 變量。

其實編譯器優化後是這樣的

  1. 配置設定一塊記憶體 M;
  2. 然後 M 的位址指派給 instance 變量;
  3. 在記憶體 M 上初始化 Singleton 對象;

這樣在第2步之後,instance其實已經有了記憶體M的引用,但是值沒有初始化,這樣在另一個線程中可以擷取到該對象,但是時沒有初始化的,比如該對象中有 int i ; i沒有初始化,調用會出錯。

synchronized關鍵字

  • synchronized的管程模型原理
  • synchronized的實作

在其他文章中有講到,這裡就不多說了。

參考書籍:java并發程式設計的藝術