Double-Check中的volatile作用

轉載自【張新強】的博客

看似完美的單例模式?

public class Single {
    private static Single3 instance;
    private Single() {}
    public static Single getInstance() {
        if (instance == null) {
            synchronized (Single.class) {
                if (instance == null) {
                    instance = new Single3();
                }
            }
        }
        return instance;
    }
}
  • 第一個if (instance == null),其實是為了解決效率問題,只有instance為null的時候,才進入synchronized的代碼段——大大減少了幾率。
  • 第二個if (instance == null),則是為了防止可能出現多個實例的情況。

那么還會有問題嗎?

答:只是『看起來』,還是有小概率出現問題的。

原子操作

原子操作(atomic)就是不可分割的操作,在計算機中,就是指不會因為線程調度打斷的操作。

例子:

m = 6; // 這是個原子操作

假如m原先的值為0,那么對于這個操作,要么執行成功m變成了6,要么是沒執行m還是0,而不會出現諸如m=3這種中間態——即使是在并發的線程中。

而,聲明并賦值就不是一個原子操作:

int n = 6; // 這不是一個原子操作

對于這個語句,至少有兩個操作:
① 明一個變量n
② 給n賦值為6
——這樣就會有一個中間狀態:變量n已經被聲明了但是還沒有被賦值的狀態。
——這樣,在多線程中,由于線程執行順序的不確定性,如果兩個線程都使用m,就可能會導致不穩定的結果出現。

指令重排

概念

簡單來說,就是計算機為了提高執行效率,會做的一些優化,在不影響最終結果的情況下,可能會對一些語句的執行順序進行調整。

例子:

int a ;   // 語句1 
a = 8 ;   // 語句2
int b = 9 ;     // 語句3
int c = a + b ; // 語句4

正常來說,對于順序結構,執行的順序是自上到下,也即1234
但是,由于指令重排的原因,因為不影響最終的結果,所以,實際執行的順序可能會變成3124或者1324
由于語句34沒有原子性的問題,語句3和語句4也可能會拆分成原子操作,再重排
——也就是說,

對于非原子性的操作,在不影響最終結果的情況下,其拆分成的原子操作可能會被重新排列執行順序

回到話題

主要在于singleton = new Singleton()這句,這并非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情:

  1. 給 singleton 分配內存
  2. 調用 Singleton 的構造函數來初始化成員變量,形成實例
  3. 將singleton對象指向分配的內存空間(執行完這步 singleton才是非 null 了)

但是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是后者,則在 3 執行完畢、2 未執行之前,被線程二搶占了,這時 instance 已經是非 null 了(但卻沒有初始化),所以線程二會直接返回 instance,然后使用,然后順理成章地報錯。

錯誤根源:

再稍微解釋一下,就是說,由于有一個『instance已經不為null但是仍沒有完成初始化』的中間狀態,而這個時候,如果有其他線程剛好運行到第一層if (instance == null)這里,這里讀取到的instance已經不為null了,所以就直接把這個中間狀態的instance拿去用了,就會產生問題。這里的關鍵在于——線程T1對instance的寫操作沒有完成,線程T2就執行了讀操作

完全版

public class Single {
    private static volatile Single4 instance;
    private Single() {}
    public static Single getInstance() {
        if (instance == null) {
            synchronized (Single.class) {
                if (instance == null) {
                    instance = new Single();
                }
            }
        }
        return instance;
    }
}

volatile發揮的作用

volatile關鍵字的一個作用是禁止指令重排,把instance聲明為volatile之后,對它的寫操作就會有一個內存屏障(什么是內存屏障?),這樣,在它的賦值完成之前,就不用會調用讀操作。

注意:volatile阻止的不是singleton = new Singleton()這句話內部[1-2-3]的指令重排而是保證了在一個寫操作([1-2-3])完成之前不會調用讀操作(if (instance == null))。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • volatile關鍵字經常在并發編程中使用,其特性是保證可見性以及有序性,但是關于volatile的使用仍然要小心...
    Ruheng閱讀 10,079評論 40 135
  • 從三月份找實習到現在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發崗...
    時芥藍閱讀 42,366評論 11 349
  • 有個國度叫快速國,那的一天只有三個小時,吃飯爸爸用一分半,媽媽用一分四十,兒子用兩分鐘。他們每分鐘的心跳是200下...
    晨光微曉閱讀 409評論 0 0
  • APP的數據指標體系主要分為五個維度,包括用戶規模與質量、參與度分析、渠道分析、功能分析以用戶屬性分析。用戶規模和...
    lj神經刀閱讀 1,289評論 0 12
  • 好多天沒寫了,最近迷上了麻將,天天忙著頂。不出錯是最好的
    宋世巍閱讀 89評論 0 0