volatile引出的單例模式之雙重檢查

volatile 作用!!!!

  1. 保證線程可見性(MESI,利用CPU的緩存一致性協議)
  2. 禁止指令重排序(CPU)

單例模式之雙重檢查

在實現單例模式時,如果未考慮多線程的情況,就容易寫出下面的錯誤代碼:

public class Singleton {
    private static Singleton uniqueSingleton;

    private Singleton() {
    }

    public Singleton getInstance() {
        if (null == uniqueSingleton) {
            uniqueSingleton = new Singleton();
        }
        return uniqueSingleton;
    }
}

在多線程的情況下,這樣寫可能會導致uniqueSingleton有多個實例。比如下面這種情況,考慮有兩個線程同時調用getInstance():

Time Thread-A Thread-B
t1 檢查到uniqueSingleton爲空
t2 檢查到uniqueSingleton爲空
t3 初始化對象A
t4 返回對象A
t5 初始化對象B
t6 返回對象B

可以看到,uniqueSingleton被實例化了兩次並且被不同對象持有。完全違背了單例的初衷。

出現上面這種情況,第一反應就是加鎖,如下:

public class Singleton {
    private static Singleton uniqueSingleton;

    private Singleton() {
    }

    public synchronized Singleton getInstance() {
        if (null == uniqueSingleton) {
            uniqueSingleton = new Singleton();
        }
        return uniqueSingleton;
    }
}

這樣雖然解決了問題,但是因爲用到了synchronized,會導致很大的性能開銷,並且加鎖其實只需要在第一次初始化的時候用到,大部分情況下都已經被初始化,之後的調用都沒必要再進行加鎖。

雙重檢查鎖

雙重檢查鎖(double checked locking)是對上述問題的一種優化。先判斷對象是否已經被初始化,再決定要不要加鎖。

public class Singleton {
    private static Singleton uniqueSingleton;

    private Singleton() {
    }

    public Singleton getInstance() {
        if (null == uniqueSingleton) {
            synchronized (Singleton.class) {
                if (null == uniqueSingleton) {
                    uniqueSingleton = new Singleton();   // error
                }
            }
        }
        return uniqueSingleton;
    }
}

如果這樣寫,運行順序就成了:

  • 檢查變量是否被初始化(不去獲得鎖),如果已被初始化則立即返回。
  • 獲取鎖。
  • 再次檢查變量是否已經被初始化,如果還沒被初始化就初始化一個對象。

執行雙重檢查是因爲,如果多個線程同時了通過了第一次檢查,並且其中一個線程首先通過了第二次檢查並實例化了對象,那麼剩餘通過了第一次檢查的線程就不會再去實例化對象。

這樣,除了初始化的時候會出現加鎖的情況,後續的所有調用都會避免加鎖而直接返回,解決了性能消耗的問題。在高併發的情況下這樣的作法是會出現問題的:

問題根源

上述寫法看似解決了問題,但是有個很大的隱患。實例化對象的那行代碼(標記爲error的那行),實際上可以分解成以下三個步驟:

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

但是有些編譯器爲了性能的原因,可能會將第二步和第三步進行重排序,順序就成了:

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

現在考慮重排序後,兩個線程發生了以下調用:

Time Thread A Thread B
t1 檢查到uniqueSingleton爲空
t2 獲取鎖
t3 再次檢查到uniqueSingleton爲空
t4 爲uniqueSingleton分配內存空間
t5 將uniqueSingleton指向內存空間
t6 檢查到uniqueSingleton不爲空
t7 訪問uniqueSingleton(此時對象還未完成初始化)
t8 初始化uniqueSingleton

在這種情況下,T7時刻線程B對uniqueSingleton的訪問,訪問的是一個初始化未完成的對象, B拿到的是一個沒有被初始化的對象,此時B線程對uniqueSingleton一通操作之後得出的的結果可能是有錯誤的(因爲uniqueSingleton 還沒完成初始化,讀出來的數據錯誤,寫回的數據也會錯誤)。

正確的雙重檢查鎖 (volatile )

public class Singleton {
    private volatile static Singleton uniqueSingleton;

    private Singleton() {
    }

    public Singleton getInstance() {
        if (null == uniqueSingleton) {
            synchronized (Singleton.class) {
                if (null == uniqueSingleton) {
                    uniqueSingleton = new Singleton();
                }
            }
        }
        return uniqueSingleton;
    }
}

爲了解決上述問題,需要在uniqueSingleton前加入關鍵字volatile。使用了volatile關鍵字後,重排序被禁止,所有的寫(write)操作都將發生在讀(read)操作之前。

至此,雙重檢查鎖就可以完美工作了。

以上是我的補充。
博客地址:https://www.cnblogs.com/xz816111/p/8470048.html

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