public class Singleton {
private volatile static Singleton instance;
private Singleton(){}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
先看這個單例模式,這就是所謂的 DCL,這裏重點不在說明什麼是DCL,這裏要說說,爲什麼 instance要用 volatile來修飾
volatile關鍵字有兩個作用
- 保證線程可見性
- 防止指令重排序
線程可見性,即線程A修改的某個變量,線程B讀取這個值,會讀到修改後的值,而不是本地緩存中的值。這裏緩存一致,通過兩個方法實現。總線加LOCK#鎖的方式(鎖總線) 或者 通過緩存一致性協議(鎖緩存),這兩個都是硬件方面的實現。加鎖的方式有性能問題,緩存一致性協議,比如MESI是這樣實現的。若某對象被volatitle修改後,主內存中就把它設置爲invalid狀態,線程B讀取時,發現是invalid狀態,就從主內在中重新讀取。
防止指令重排序, 是JVM層級,是通過內存屏障來實現的。並且是讀寫都有內存屏障
明白了這些,來說說DCL那個問題,爲什麼要用volatile關鍵字,就是防止指令重排序。1.對象創建先分配內存,2、再初始化相關數據,3、最後將引用指向堆內地址。其中第二個步驟會複雜的多,由於指令重排序的可能,最後一步可能先於第二步執行完成。
問題就來了,第一個線程進來調用,調用創建對象的方法,多線程情況下,其它進來的線程會阻塞。但創建對象的過程中,可能發生指令重排序,第3步先於第2步完成,這時候又有一個線程進來,調用第一個if決斷,對象不等於null,直接反一個半初始化的對象。這就是問題,雖然發生的概率很小。加上volatile,杜絕第3步先於第2步完成的情況,這樣就不可能返回一個半初始化的對象。