Java併發學習 3 :volatile的應用

在Java併發編程中,synchronized和volatile 都扮演着重要的角色,volatile是輕量級的synchronzied,其在多處理器開發時保證了共享變量的"可見性".

問題引入:多個CPU的不可見性造成髒讀

我們知道CPU速度非常快,比內存快百倍以上,所以CPU更希望和速度相近的CPU cache打交道。

而一個多核的CPU本質上就是多個CPU共用一個外殼,每個核就是一個單核CPU,其都有屬於自己的cache。

當更改一個變量時,CPU將值寫入到對應的cache中,不一定寫入內存中,這個CPU認爲值已經改變了,但是其他CPU認爲值沒有改變。這樣就會出現髒讀。

解決方式:volatile 指令實現可見性

volatile 底層是使用Lock指令。

CPU讀取到lock指令後,會做以下操作。

1. 將對應的值先到CPU對應的cache中

2. 將對應的值寫入到內存中(就是內存條,我們稱其爲系統內存)

3. 第二部的寫操作會使在其他CPU裏緩存了該地址的數據無效。

4. 其他CPU當需要這個數值時,必須去重新讀取內存。

這樣就保證了多CPU之間的可見性。

問題引入:指令重排序造成線程安全問題

JVM會在不影響單線程運行結果的準則下,對代碼進行重排序。舉個例子,

public void writer() {
    a = 1;                   //1
    flag = true;             //2
}

 雖然你寫的代碼是按照,"1","2"排序,但是在JVM編譯後,可能順序就是"2","1"等.

JVM爲什麼這樣做?

public void writer() {
    int i = 0;   
     a = 1;                   //1
    while (i++ != 2){
        flag = true;             //2
        a++;
    }

}

 很明顯,循環每次都會調用flag = true,完全不必要,可以將2插入到1前後.可以增加運行效率。

爲什麼會有線程安全問題?

簡單來說,代碼在多線程下本來就難以掌控,重排序往往會造成一些意想不到的問題。

因爲我們寫出來的多線程代碼可能會依賴於特定的代碼順序,如果更改,那麼會導致線程安全問題。

經典案例:基於雙重檢查的單例模式

單例模式就是保證一個類始終只有一個對象的一種編碼方法。我們可以很容易的寫出來一個單線程下的單例餓漢模式

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
  
    public static Singleton getInstance() {  
        if (instance == null) {  
            // 可能同時有多個線程進入if中
            instance = new Singleton();  
        }  
        return instance;  
    }  
}

在多線程時,可以會new出多個實例.

我們可以通過synchronized關鍵子鎖住這個方法,保證其線程安全。

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
    public static synchronized Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
    }  
}

synchronized是把這個方法鎖成一個單線程方法,效率降低十分明顯。

我們通過volatile進行優化,將其synchronized鎖的粒度減小。

// 3 所在行其實有三句話

  1. 分配內存空間
  2. 將對象指向剛分配的內存空間
  3. 初始化對象

JVM會對代碼進行重排序(爲了優化速度)

Time Thread A Thread B
T1 檢查到uniqueSingleton爲空  
T2 獲取鎖  
T3 再次檢查到uniqueSingleton爲空  
T4 uniqueSingleton分配內存空間  
T5 uniqueSingleton指向內存空間  
T6   檢查到uniqueSingleton不爲空
T7   訪問uniqueSingleton(此時對象還未完成初始化)
T8 初始化uniqueSingleton  

所以需要使用volatile關鍵字,防止指令重排序導致的線程安全問題。

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章