掃盲細節,到底該如何正確地寫出單例模式?
單例模式算是設計模式中最容易理解,也是最容易手寫程式碼的模式,但是其中的坑卻不少,很多都是一些老生常談的問題,如何建立一個執行緒安全的單例?什麼是雙檢鎖?我們知道單例模式一般分兩種,即懶漢式和餓漢式,以下逐一分析。
懶漢式,執行緒不安全
|
這段程式碼簡單明瞭,而且使用了懶載入,但是卻存在致命的問題。當有多個執行緒並行呼叫 getInstance() 的時候,就會建立多個例項,也就是說在多執行緒下不能達到僅存在一個例項的效果。
懶漢式,執行緒安全
為了解決上面的問題,最簡單的方法是將getInstance() 方法設為同步(synchronized)。
|
雖然做到了執行緒安全,解決了多例項的問題,但它並不高效。
雙重檢驗鎖模式實現單例
雙重檢驗鎖模式是一種使用同步程式碼塊加鎖的方法,會有兩次檢查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 件事:
- 給singleton分配記憶體
- 呼叫Singleton的建構函式來初始化成員變數
- 將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()之前必須呼叫某個方法設定引數。