單例模式中volatile關鍵字的作用


背景&問題

在早期的JVM中,synchronized存在巨大的性能開銷。因此,有人想出了一個“聰明”的技巧:雙重檢查鎖定(Double-Checked Locking)。人們想通過雙重檢查鎖定來降低同步的開銷。下面是使用雙重檢查鎖定來實現延遲初始化的示例代碼。

public class DoubleCheckedLocking { // 1
    private static Instance instance; // 2
    public static Instance getInstance() { // 3
        if (instance == null) { // 4:第一次檢查
            synchronized (DoubleCheckedLocking.class) { // 5:加鎖
                if (instance == null) // 6:第二次檢查
                instance = new Instance(); // 7:問題的根源出在這里
            } // 8
        } // 9
        return instance; // 10
    } // 11
}

上述的Instance類變量是沒有用volatile關鍵字修飾的,會導致這樣一個問題:
在線程執行到第4行的時候,代碼讀取到instance不為null時,instance引用的對象有可能還沒有完成初始化。

原因

主要的原因是重排序。重排序是指編譯器和處理器為了優化程序性能而對指令序列進行重新排序的一種手段。
第7行的代碼創建了一個對象,這一行代碼可以分解成3個操作:

memory = allocate();  // 1:分配對象的內存空間
ctorInstance(memory); // 2:初始化對象
instance = memory;  // 3:設置instance指向剛分配的內存地址

根源在於代碼中的2和3之間,可能會被重排序。例如:

memory = allocate();  // 1:分配對象的內存空間
instance = memory;  // 3:設置instance指向剛分配的內存地址
// 注意,此時對象還沒有被初始化!
ctorInstance(memory); // 2:初始化對象java

這在單線程環境下是沒有問題的,但在多線程環境下會出現問題:B線程會看到一個還沒有被初始化的對象。
A2和A3的重排序不影響線程A的最終結果,但會導致線程B在B1處判斷出instance不為空,線程B接下來將訪問instance引用的對象。此時,線程B將會訪問到一個還未初始化的對象。

改進

所以只需要做一點小的修改(把instance聲明為volatile型),就可以實現線程安全的延遲初始化。因為被volatile關鍵字修飾的變量是被禁止重排序的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM