java happen before

下面是Java內存模型中的八條可保證happen—before的規則java

一、程序次序規則:在一個單獨的線程中,按照程序代碼的執行流順序,(時間上)先執行的操做happen—before(時間上)後執行的操做。數組

    二、管理鎖定規則:一個unlock操做happen—before後面(時間上的前後順序,下同)對同一個鎖的lock操做。安全

    三、volatile變量規則:對一個volatile變量的寫操做happen—before後面對該變量的讀操做。多線程

    四、線程啓動規則:Thread對象的start()方法happen—before此線程的每個動做。app

    五、線程終止規則:線程的全部操做都happen—before對此線程的終止檢測,能夠經過Thread.join()方法結束、Thread.isAlive()的返回值等手段檢測到線程已經終止執行。dom

    六、線程中斷規則:對線程interrupt()方法的調用happen—before發生於被中斷線程的代碼檢測到中斷時事件的發生。函數

    七、對象終結規則:一個對象的初始化完成(構造函數執行結束)happen—before它的finalize()方法的開始。this

    八、傳遞性:若是操做A happen—before操做B,操做B happen—before操做C,那麼能夠得出A happen—before操做C。線程

 

」一個操做時間上先發生於另外一個操做「並不表明」一個操做happen—before另外一個操做「。對象

解決方法:能夠將setValue(int)方法和getValue()方法均定義爲synchronized方法,也能夠把value定義爲volatile變量(value的修改並不依賴value的原值,符合volatile的使用場景),分別對應happen—before規則的第2和第3條。注意,只將setValue(int)方法和getvalue()方法中的一個定義爲synchronized方法是不行的,必須對同一個變量的全部讀寫同步,才能保證不讀取到陳舊的數據,僅僅同步讀或寫是不夠的 。

」一個操做happen—before另外一個操做「並不表明」一個操做時間上先發生於另外一個操做「。

 

DCL即雙重檢查加鎖

public class LazySingleton {
    private int someFiled;
    private static LazySingleton instance;

    private LazySingleton() {
        this.someFiled = new Random().nextInt(200) + 1; // 1
    }

    // 不能徹底保證多線程場景下someField值的同步
    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 getSomeFiled() {
        return this.someFiled;                          // 7
    }
}

這裏獲得單一的instance實例是沒有問題的,問題的關鍵在於儘管獲得了Singleton的正確引用,可是卻有可能訪問到其成員變量的不正確值。具體來講Singleton.getInstance().getSomeField()有可能返回someField的默認值0。若是程序行爲正確的話,這應當是不可能發生的事,由於在構造函數裏設置的someField的值不可能爲0。爲也說明這種狀況理論上有可能發生,咱們只須要說明語句(1)和語句(7)並不存在happen-before關係。

假設線程Ⅰ是初次調用getInstance()方法,緊接着線程Ⅱ也調用了getInstance()方法和getSomeField()方法,咱們要說明的是線程Ⅰ的語句(1)並不happen-before線程Ⅱ的語句(7)。線程Ⅱ在執行getInstance()方法的語句(2)時,因爲對instance的訪問並無處於同步塊中,所以線程Ⅱ可能觀察到也可能觀察不到線程Ⅰ在語句(5)時對instance的寫入,也就是說instance的值可能爲空也可能爲非空。咱們先假設instance的值非空,也就觀察到了線程Ⅰ對instance的寫入,這時線程Ⅱ就會執行語句(6)直接返回這個instance的值,而後對這個instance調用getSomeField()方法,該方法也是在沒有任何同步狀況被調用,所以整個線程Ⅱ的操做都是在沒有同步的狀況下調用 ,這時咱們便沒法利用上述8條happen-before規則獲得線程Ⅰ的操做和線程Ⅱ的操做之間的任何有效的happen-before關係(主要考慮規則的第2條,但因爲線程Ⅱ沒有在進入synchronized塊,所以不存在lock與unlock鎖的問題),這說明線程Ⅰ的語句(1)和線程Ⅱ的語句(7)之間並不存在happen-before關係,這就意味着線程Ⅱ在執行語句(7)徹底有可能觀測不到線程Ⅰ在語句(1)處對someFiled寫入的值,這就是DCL的問題所在。很荒謬,是吧?DCL本來是爲了逃避同步,它達到了這個目的,也正是由於如此,它最終受到懲罰,這樣的程序存在嚴重的bug,雖然這種bug被發現的機率絕對比中彩票的機率還要低得多,並且是轉瞬即逝,更可怕的是,即便發生了你也不會想到是DCL所引發的。

    前面咱們說了,線程Ⅱ在執行語句(2)時也有可能觀察空值,若是是種狀況,那麼它須要進入同步塊,並執行語句(4)。在語句(4)處線程Ⅱ還可以讀到instance的空值嗎?不可能。這裏由於這時對instance的寫和讀都是發生在同一個鎖肯定的同步塊中,這時讀到的數據是最新的數據。爲也加深印象,我再用happen-before規則分析一遍。線程Ⅱ在語句(3)處會執行一個lock操做,而線程Ⅰ在語句(5)後會執行一個unlock操做,這兩個操做都是針對同一個鎖--Singleton.class,所以根據第2條happen-before規則,線程Ⅰ的unlock操做happen-before線程Ⅱ的lock操做,再利用單線程規則,線程Ⅰ的語句(5) -> 線程Ⅰ的unlock操做,線程Ⅱ的lock操做 -> 線程Ⅱ的語句(4),再根據傳遞規則,就有線程Ⅰ的語句(5) -> 線程Ⅱ的語句(4),也就是說線程Ⅱ在執行語句(4)時可以觀測到線程Ⅰ在語句(5)時對Singleton的寫入值。接着對返回的instance調用getSomeField()方法時,咱們也能獲得線程Ⅰ的語句(1) -> 線程Ⅱ的語句(7)(因爲線程Ⅱ有進入synchronized塊,根據規則2可得),這代表這時getSomeField可以獲得正確的值。可是僅僅是這種狀況的正確性並不妨礙DCL的不正確性,一個程序的正確性必須在全部的狀況下的行爲都是正確的,而不能有時正確,有時不正確。

    對DCL的分析也告訴咱們一條經驗原則:對引用(包括對象引用和數組引用)的非同步訪問,即便獲得該引用的最新值,卻並不能保證也能獲得其成員變量(對數組而言就是每一個數組元素)的最新值。

解決方案:

    一、最簡單並且安全的解決方法是使用static內部類的思想,它利用的思想是:一個類直到被使用時才被初始化,而類初始化的過程是非並行的,這些都有JLS保證。

以下述代碼:

public class Singleton {
    private Singleton() {
    }

    private static class InstanceHolder {
        private static final Singleton instance = new Singleton();
    }

    private static Singleton getInstance() {
        return InstanceHolder.instance;
    }
}

二、另外,能夠將instance聲明爲volatile,即

private volatile static LazySingleton instance; 

    這樣咱們即可以獲得,線程Ⅰ的語句(5) -> 語線程Ⅱ的句(2),根據單線程規則,線程Ⅰ的語句(1) -> 線程Ⅰ的語句(5)和線程Ⅱ的語句(2) -> 線程Ⅱ的語句(7),再根據傳遞規則就有線程Ⅰ的語句(1) -> 線程Ⅱ的語句(7),這表示線程Ⅱ可以觀察到線程Ⅰ在語句(1)時對someFiled的寫入值,程序可以獲得正確的行爲。

   注:

    一、volatile屏蔽指令重排序的語義在JDK1.5中才被徹底修復,此前的JDK中即便將變量聲明爲volatile,也仍然不能徹底避免重排序所致使的問題(主要是volatile變量先後的代碼仍然存在重排序問題),這點也是在JDK1.5以前的Java中沒法安全使用DCL來實現單例模式的緣由。

    二、把volatile寫和volatile讀這兩個操做綜合起來看,在讀線程B讀一個volatile變量後,寫線程A在寫這個volatile變量以前,全部可見的共享變量的值都將當即變得對讀線程B可見。

   三、 在java5以前對final字段的同步語義和其它變量沒有什麼區別,在java5中,final變量一旦在構造函數中設置完成(前提是在構造函數中沒有泄露this引用),其它線程一定會看到在構造函數中設置的值。而DCL的問題正好在於看到對象的成員變量的默認值,所以咱們能夠將LazySingleton的someField變量設置成final,這樣在java5中就可以正確運行了。

相關文章
相關標籤/搜索