轉載請注明出處:http://blog.csdn.net/ns_code/article/details/17348313
happen—before規則介紹
Java語言中有一個“先行發生”(happen—before)的規則,它是Java內存模型中定義的兩項操作之間的偏序關系,如果操作A先行發生於操作B,其意思就是說,在發生操作B之前,操作A產生的影響都能被操作B觀察到,“影響”包括修改了內存中共享變量的值、發送了消息、調用了方法等,它與時間上的先后發生基本沒有太大關系。這個原則特別重要,它是判斷數據是否存在競爭、線程是否安全的主要依據。
舉例來說,假設存在如下三個線程,分別執行對應的操作:
---------------------------------------------------------------------------
線程A中執行如下操作:i=1
線程B中執行如下操作:j=i
線程C中執行如下操作:i=2
---------------------------------------------------------------------------
假設線程A中的操作”i=1“ happen—before線程B中的操作“j=i”,那么就可以保證在線程B的操作執行后,變量j的值一定為1,即線程B觀察到了線程A中操作“i=1”所產生的影響;現在,我們依然保持線程A和線程B之間的happen—before關系,同時線程C出現在了線程A和線程B的操作之間,但是C與B並沒有happen—before關系,那么j的值就不確定了,線程C對變量i的影響可能會被線程B觀察到,也可能不會,這時線程B就存在讀取到不是最新數據的風險,不具備線程安全性。
下面是Java內存模型中的八條可保證happen—before的規則,它們無需任何同步器協助就已經存在,可以在編碼中直接使用。如果兩個操作之間的關系不在此列,並且無法從下列規則推導出來的話,它們就沒有順序性保障,虛擬機可以對它們進行隨機地重排序。
1、程序次序規則:在一個單獨的線程中,按照程序代碼的執行流順序,(時間上)先執行的操作happen—before(時間上)后執行的操作。
2、管理鎖定規則:一個unlock操作happen—before后面(時間上的先后順序,下同)對同一個鎖的lock操作。
3、volatile變量規則:對一個volatile變量的寫操作happen—before后面對該變量的讀操作。
4、線程啟動規則:Thread對象的start()方法happen—before此線程的每一個動作。
5、線程終止規則:線程的所有操作都happen—before對此線程的終止檢測,可以通過Thread.join()方法結束、Thread.isAlive()的返回值等手段檢測到線程已經終止執行。
6、線程中斷規則:對線程interrupt()方法的調用happen—before發生於被中斷線程的代碼檢測到中斷時事件的發生。
7、對象終結規則:一個對象的初始化完成(構造函數執行結束)happen—before它的finalize()方法的開始。
8、傳遞性:如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C。
時間上先后順序和happen—before原則
”時間上執行的先后順序“與”happen—before“之間有何不同呢?
1、首先來看操作A在時間上先與操作B發生,是否意味着操作A happen—before操作B?
一個常用來分析的例子如下:
private int value = 0; public int get(){ return value; } public void set(int value){ this.value = value; }
解決方法:可以將setValue(int)方法和getValue()方法均定義為synchronized方法,也可以把value定義為volatile變量(value的修改並不依賴value的原值,符合volatile的使用場景),分別對應happen—before規則的第2和第3條。注意,只將setValue(int)方法和getvalue()方法中的一個定義為synchronized方法是不行的,
2、其次來看,操作A happen—before操作B,是否意味着操作A在時間上先與操作B發生?
看有如下代碼:
- x = 1;
- y = 2;
假設同一個線程執行上面兩個操作:操作A:x=1和操作B:y=2。根據happen—before規則的第1條,操作A happen—before 操作B,但是由於編譯器的指令重排序(Java語言規范規定了JVM線程內部維持順序化語義,也就是說只要程序的最終結果等同於它在嚴格的順序化環境下的結果,那么指令的執行順序就可能與代碼的順序不一致。這個過程通過叫做指令的重排序。指令重排序存在的意義在於:JVM能夠根據處理器的特性(CPU的多級緩存系統、多核處理器等)適當的重新排序機器指令,使機器指令更符合CPU的執行特點,最大限度的發揮機器的性能。在沒有同步的情況下,編譯器、處理器以及運行時等都可能對操作的執行順序進行一些意想不到的調整)等原因,操作A在時間上有可能后於操作B被處理器執行,但這並不影響happen—before原則的正確性。
因此,”一個操作happen—before另一個操作“並不代表”一個操作時間上先發生於另一個操作“。
最后,一個操作和另一個操作必定存在某個順序,要么一個操作或者是先於或者是后於另一個操作,或者與兩個操作同時發生。同時發生是完全可能存在的,特別是在多CPU的情況下。而兩個操作之間卻可能沒有happen-before關系,也就是說有可能發生這樣的情況,操作A不happen-before操作B,操作B也不happen-before操作A,用數學上的術語happen-before關系是個偏序關系。兩個存在happen-before關系的操作不可能同時發生,一個操作A happen-before操作B,它們必定在時間上是完全錯開的,這實際上也是同步的語義之一(獨占訪問)。
利用happen—before規則分析DCL
public class LazySingleton { private int someField; private static LazySingleton instance; private LazySingleton() { this.someField = new Random().nextInt(200)+1; // (1) } public static LazySingleton getInstance() { if (instance == null) { // (2) synchronized(LazySingleton.class) { // (3) if (instance == null) { // (4) instance = new LazySingleton(); // (5) } } } return instance; // (6) } public int getSomeField() { return this.someField; // (7) } }
public class Singleton { private Singleton() {} // Lazy initialization holder class idiom for static fields private static class InstanceHolder { private static final Singleton instance = new Singleton(); } public static Singleton getSingleton() { return InstanceHolder.instance; } }
這樣我們便可以得到,線程Ⅰ的語句(5) -> 語線程Ⅱ的句(2),根據單線程規則,線程Ⅰ的語句(1) -> 線程Ⅰ的語句(5)和語線程Ⅱ的句(2) -> 語線程Ⅱ的句(7),再根據傳遞規則就有線程Ⅰ的語句(1) -> 語線程Ⅱ的句(7),這表示線程Ⅱ能夠觀察到線程Ⅰ在語句(1)時對someFiled的寫入值,程序能夠得到正確的行為。
注:
1、volatile屏蔽指令重排序的語義在JDK1.5中才被完全修復,此前的JDK中及時將變量聲明為volatile,也仍然不能完全避免重排序所導致的問題(主要是volatile變量前后的代碼仍然存在重排序問題),這點也是在JDK1.5之前的Java中無法安全使用DCL來實現單例模式的原因。
2、把volatile寫和volatile讀這兩個操作綜合起來看,在讀線程B讀一個volatile變量后,寫線程A在寫這個volatile變量之前,所有可見的共享變量的值都將立即變得對讀線程B可見。
3、 在java5之前對final字段的同步語義和其它變量沒有什么區別,在java5中,final變量一旦在構造函數中設置完成(前提是在構造函數中沒有泄露this引用),其它線程必定會看到在構造函數中設置的值。而DCL的問題正好在於看到對象的成員變量的默認值,因此我們可以將LazySingleton的someField變量設置成final,這樣在java5中就能夠正確運行了。