單例模式下為什麼一定要加volatile關鍵字

一個不願透漏真實姓名的碼農發表於2020-11-19

有一道面試題,單例模式已經雙重檢查鎖定(Double-Check-Lock)了,要不要加volatile關鍵字。以下是雙重檢查鎖定來實現單例模式程式碼

public class DoubleCheckLock {

    private static DoubleCheckLock INSTANCE;

    private DoubleCheckLock() {

    }

    public static DoubleCheckLock getInstance() {
        if (INSTANCE == null) {
            synchronized (DoubleCheckLock.class) {
                if (INSTANCE == null) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new DoubleCheckLock();
                }
            }
        }
        return INSTANCE;
    }
}

先介紹下volatile關鍵字的特性,一個變數被volatile修飾後具備兩個特性

  • 可見性:此變數對所有執行緒可見,當一個執行緒修改了這個變數的值,新值對其他執行緒是可以立即得知的,如果不是volatile修飾的變數的值線上程間傳遞均需要通過主記憶體來完成,比如執行緒A修改了普通變數的值,然後向主記憶體進行回寫,另一條執行緒B線上程A回寫完成之後再對主記憶體進行讀取操作,新變數值才會對執行緒B可見。
  • 禁止指令重排序優化:普通的變數僅會保證該方法的執行過程中所有依賴賦值結果的地方都能獲取到正確的結果,而不能保證變數賦值操作的順序與程式程式碼中的執行順序一致。

先試著解釋下如果不加volatile會有什麼問題,第一個執行緒過來,檢查發現其他執行緒沒有初始化然後就加上鎖,上鎖後對這個INSTANCE進行初始化,可是在這個過程中我們new了這個物件並且申請了記憶體,申請完記憶體裡邊的成員變數已經賦了初始值0,還沒有進行初始化,但這個時候INSTANCE就已經指向記憶體了,所以這個INSTATNCE已經不等於空了,這種情況下里一個執行緒來了,來了之後他首先執行if (INSTANCE == null),這個時候他處於半初始化,不為空的狀態,第二個執行緒就直接使用這個初始值了,而不是用那個預設值,解決這個問題就要加上volatile。

使用jclasslib檢視getInstance方法的位元組碼,下面是 INSTANCE = new DoubleCheckLock(); 的位元組碼指令

new
dup
invokespecial 
putstatic
  • new指令在java堆上為物件分配記憶體空間,並將地址壓入運算元棧頂
  • dup指令為複製運算元棧頂值,並將其壓入棧定,這是運算元棧有連續相同的兩個物件地址
  • invokespecial呼叫例項的建構函式,這時會彈出一個棧頂元素
  • putstatic指令將物件地址賦值給變數t,這時也會彈出一個棧頂元素

invokespecial指令與putstatic指令發生重排序,使這個INSTANTCE進行了半初始化,才會導致出現問題

使用volatile主要是加了記憶體屏障,指令重排序時不能把後面的指令重排序到記憶體屏障之前的位置

public class DoubleCheckLock {

    private volatile static DoubleCheckLock INSTANCE;

    private DoubleCheckLock() {

    }

    public static DoubleCheckLock getInstance() {
        if (INSTANCE == null) {
            synchronized (DoubleCheckLock.class) {
                if (INSTANCE == null) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new DoubleCheckLock();
                }
            }
        }
        return INSTANCE;
    }
}

相關文章