1. 程式人生 > 實用技巧 >掃盲細節,到底該如何正確地寫出單例模式?

掃盲細節,到底該如何正確地寫出單例模式?

單例模式算是設計模式中最容易理解,也是最容易手寫程式碼的模式,但是其中的坑卻不少,很多都是一些老生常談的問題,如何建立一個執行緒安全的單例?什麼是雙檢鎖?我們知道單例模式一般分兩種,即懶漢式和餓漢式,以下逐一分析。

懶漢式,執行緒不安全

public class Singleton {
    private static Singleton instance;
    private Singleton (){}

    public static Singleton getInstance() {
     if (instance == null) {
         instance = new Singleton();
     }
     return instance;
    }
}
複製程式碼

這段程式碼簡單明瞭,而且使用了懶載入,但是卻存在致命的問題。當有多個執行緒並行呼叫 getInstance() 的時候,就會建立多個例項,也就是說在多執行緒下不能達到僅存在一個例項的效果。

懶漢式,執行緒安全

為了解決上面的問題,最簡單的方法是將getInstance() 方法設為同步(synchronized)。

public static synchronized Singleton getInstance() {
    if (instance == null) {
        instance = new Singleton();
    }
    return instance;
}
複製程式碼

雖然做到了執行緒安全,解決了多例項的問題,但它並不高效。

雙重檢驗鎖模式實現單例

雙重檢驗鎖模式是一種使用同步程式碼塊加鎖的方法,會有兩次檢查instance==null,一次在同步塊外,一次在同步塊內。那為什麼在同步塊內還要校驗一次呢?是因為可能會有多個執行緒一起進入同步塊外的if,如果在同步塊內不進行二次校驗的話就可能出現多個例項。

public static Singleton getInstance() {
    if (singleton == null) {
        synchronized (Singleton.class) {
            if (singleton == null) {
                singleton = new Singleton();
            }
        }
    }
    return singleton;
}複製程式碼

很遺憾,以上方式也不是很完美,問題在於singleton= new Singleton()這段程式碼,這並非是一個原子操作,事實上在 JVM 中對這段程式碼大概做了3 件事:

  1. 給singleton分配記憶體
  2. 呼叫Singleton的建構函式來初始化成員變數
  3. 將singleton物件指向分配的記憶體空間

但是在JVM的即時編譯器中存在指令重排序的優化,也就是上面的第二步和第三步的執行順序得不到保證,最終執行順序可能是1-2-3也可能是1-3-2,如果是後者的話,則3執行完畢且2未執行之前,getInstance()被其他執行緒呼叫,這時singleton已經不是null,但卻沒有初始化直接返回singleton然後使用,此時就會報錯。為了解決這個問題,我們需要將singleton宣告為volatile就行了。

public class Singleton {
    private static volatile Singleton singleton;
    private Singleton() {}

    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}複製程式碼

使用volatile不僅僅是保證執行緒在本地不會有singleton副本,每次去記憶體中讀取,還有另一個重要特性:禁止指令重排序優化。

餓漢式

這種方式很簡單,單例的例項被宣告成了static和final,在第一次載入到記憶體中就會被初始化,所以建立的例項本身是執行緒安全的。

public class Singleton {
    public static final Singleton singleton = new Singleton();
    private Singleton() {}

    public static Singleton getInstance() {
        return singleton;
    }
}複製程式碼

餓漢式的缺點是它不是懶載入模式,單例會在載入類後一開始就被初始化。且這種模式在某些場景下無法使用,比如Singleton例項的建立時依賴引數或者配置檔案,在getInstance()之前必須呼叫某個方法設定引數。