第003彈:懶漢型單例模式的演變

這篇文章主要是爲了從頭開始,詳細介紹懶漢模式的實現,以及實現的緣由。html

以前寫過一篇比較淺的懶漢模式,能夠優先參照:設計模式(一)單例模式:2-懶漢模式java

 

Step1:基礎的懶漢模式設計模式

public class LazySingleton {

    private static LazySingleton instance = null;

    private LazySingleton() {
    }

    public static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

基礎的懶漢模式保證了在調用 getInstance() 方法的時候才第一次初始化單例對象。多線程

可是這麼作沒法保證在多線程環境下只建立一個對象。post

顯然,假設有多個線程同時調用 getInstance() 方法,在第一個線程執行完畢以前,會有多個 LazyInstance 對象被建立。性能

 

Step2:爲 getInstance() 方法加上同步鎖url

public class LazySingleton {

    private static LazySingleton instance = null;

    private LazySingleton() {
    }

    public synchronized static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

經過簡單地在方法上加上同步鎖,能夠保證同時只有一個線程調用這個靜態方法,從而保證在多線程環境下的單例。spa

然而這麼作有明顯的 performance 隱患。線程

假設有多個線程想要獲取 instance,不管此時對象是否已經被建立,都要頻繁地獲取鎖,釋放鎖。這種作法很影響效率。翻譯

 

Step3:在 getInstance() 方法內部增長同步代碼塊

public class LazySingleton {

    private static LazySingleton instance = null;

    private LazySingleton() {
    }

    public static LazySingleton getInstance() {
        if (instance == null) {
            synchronized (LazySingleton.class) {
                instance = new LazySingleton();
            }
        }
        return instance;
    }
}

既然在方法上加同步鎖不合適,那麼就在方法內部增長同步代碼塊。

在判斷 instance == null 以後,增長的同步代碼塊就不會產生 performance 問題,由於以後的訪問會直接 return,不會進入同步代碼塊。

可是這麼作,不能完整地保證單例。

參照 Step1,假設有多線程調用,且都經過了 instance == null 的判斷,那麼同樣會有多個 LazySingleton 對象被建立。

 

Step4:使用 Double-Checked Locking

public class LazySingleton {

    private static LazySingleton instance = null;

    private LazySingleton() {
    }

    public static LazySingleton getInstance() {
        if (instance == null) {
            synchronized (LazySingleton.class) {
                if (instance == null) {
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }
}

經過增長雙重判斷,以及同步代碼塊,就能夠避免 Step3 中可能出現的隱患。

可是 Double-Checked Locking 雖然可以保證單例的建立,可是在多線程的狀況下可能出現某個線程使用建立不徹底的對象的狀況。

 

Step5:使用 volatile 關鍵字修飾字段 instance

public class LazySingleton {

    private static volatile LazySingleton instance = null;

    private LazySingleton() {
    }

    public static LazySingleton getInstance() {
        if (instance == null) {
            synchronized (LazySingleton.class) {
                if (instance == null) {
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }
}

 

參考文檔:The "Double-Checked Locking is Broken" Declaration

若是不適應英文描述,ImportNew 對這篇文檔進行了翻譯:能夠不要再使用Double-Checked Locking了

 

這裏面講述了 Double-Checked Locking 在懶漢模式下可能出現的問題。

主要問題在於 Java 指令重排。

當 Java 代碼被編譯器翻譯成字節碼被存儲在 JVM 時,爲了提升性能,編譯器會對這些操做指令進行指令重排。

也就是說,代碼在計算機上執行的順序,會被打亂。

返回到本例的問題,懶漢模式最關鍵的2個操做:

  1. 在 heap 中建立一個 LazyInstance 對象。
  2. 爲字段 instance 賦值。

假設操做1在操做2以前被執行,那麼代碼就沒有問題。

反之若操做2在操做1以前被執行,若是不能保證建立 LazyInstance 對象的過程是原子的,那麼代碼仍是會出現問題,由於 instance 指向了一個沒有被建立徹底的對象。

事實上,引用類型和64位類型(long 和 double)都不能被原子地讀寫。

解決方案是經過 volatile 關鍵字來禁止指令重排(這是 volatile 的兩個做用之一,另外一個做用是保證共享變量的可見性,這裏不深刻展開)

相關文章
相關標籤/搜索