單例模式筆記
單例模式是限制類的實例只有一個的設計模式。
代碼
單線程下的單例模式代碼
public class SimpleSingleton { private static SimpleSingleton simpleSingleton; ? private SimpleSingleton() {} ? public static SimpleSingleton getInstance() { // 如果 simpleSingleton 未進行實例化,則創建實例,之後都使用此實例 if (simpleSingleton == null) { simpleSingleton= new SimpleSingleton(); } return simpleSingleton; } }
但是如果在多線程的情況下,可能出現在第一個線程 A 線程判斷simpleSingleton
為空時,進入simpleSingleton = new Simpleton();
部分進行代碼實例化,但在 A 線程還未實例化結束時,另一個線程 B 線程進行判斷simpleSingleton
是否為空,此時由於 A 線程還沒有實例化賦值給simpleSingleton
,所以其仍未null
,這是 B 線程也會進入執行simpleSingleton = new Simpleton();
多線程下的單例模式代碼
多線程下最簡單的方式是對getInstance()
方法整個加上synchronized
修飾符,保證每次只有一個線程能進入此方法。但是這樣做會很影響性能,我們應該盡量減小鎖作用的範圍,所以最好采用如下的雙重加鎖方式:
public class SynchronizedSingleton { ? private static SynchronizedSingleton singleton; ? private SynchronizedSingleton() {} ? public staticSynchronizedSingleton getInstance() { if (singleton == null) { synchronized (SynchronizedSingleton.class) { if (singleton == null) { singleton = new SynchronizedSingleton(); } } } return singleton; } }
之所以進行第二次singleton == null
判斷是因為在線程 A 在第一次進行判斷為null
後獲得鎖進行實例化,在實例化未完成時,B 線程判斷仍為null
,這是由於獲得不了鎖,所以等待,在 A 線程創建實例後釋放了鎖,這時 B 線程獲得鎖並執行,如果此時不進行第二次的singleton == null
判斷,則 B 線程也會創建一個新的實例,導致單例模式出現問題;而如果進行第二次判斷,則會得知singleton
已經被實例化,就不會再創建新的實例。
目前一切看起來都很美好,但是仍然有一個問題需要解決。
創建對象實例可以分為三個步驟:
-
分配內存
-
調用構造函數
-
將對象指向分配的內存地址
之前的代碼如果依此順序執行,則不會有問題。但是為了提高性能,編譯器和處理器常常會對指令進行重排序,這時如果步驟 2 和步驟 3 的順序顛倒了,先將對象指向分配的內存地址,後執行構造函數那麽就會出現問題。當 A 線程將對象指向分配的內存地址,但還未執行構造函數的時候, B 線程進入,判斷對象不為空,則將對象引用返回,這時如果使用此引用,則會出現問題。
目前有三個解決方法:
1. 給靜態實例屬性加上 volatile關鍵字(需要 JDK 1.5 及之後版本)
private static volatile SynchronizedSingleton singleton;
volatile 關鍵字可以保證對 volatile 變量的操作不會進行重排序。
2. 使用單個元素的枚舉類型(需要 JDK 1.5 及之後版本)
public enum Singleton { INSTANCE; }
3. 使用子類,由 JVM 保證單例
public class InnerClassSingleton { public static Singleton getInstance() { return Singleton.singleton; } ? private static class Singleton { static Singleton singleton = new Singleton(); } }
類的靜態屬性只會在第一次加載的時候初始化一次,同時 JVM 保證在初始化的過程中(未完成時)無法被使用。
單例模式筆記