單例模式幾種實現方式
1、餓漢式:靜態常量
特點:單例的實例被聲明成static和final變量了,在第一次加載類到內存中時就會初始化,所以會創建實例本身是線程安全的
public class Singleton { private final static Singleton instance = new Singleton(); private Singleton(){ } public static Singleton getInstance(){ return instance; } }
2、懶漢式:線程不安全
特點:使用了懶加載模式,但是卻存在致命的問題。當多個線程並行調用getInstance()的時候,就會創建多個實例,即在多線程下不能正常工作
public class Singleton { private static Singleton instance = null; private Singleton(){ } public static Singleton getInstance(){ if(null == instance) { instance = new Singleton(); }return instance; } }
3、懶漢式:線程安全
特點:線程安全,並且解決了多實例的問題,但是它並不高效。因為在任何時候只能有一個線程調用getInstance()方法,但是同步操作只需要在第一次調用時才被需要,即第一次創建單例實例對象時
public class Singleton { private static Singleton instance = null; private Singleton(){ } public static synchronized Singleton getInstance(){if(null == instance) { instance = new Singleton(); } return instance; } }
4、懶漢式:靜態內部類
特點:使用JVM本身機制保證了線程安全問題;由於SingleHolder是私有的,除了getInstacne()之外沒有辦法訪問它,因此它是懶漢式的;同時讀取實例的時候不會進行同步,沒有性能缺陷,也不依賴JDK版本
public class Singleton { private Singleton(){ } private static class SingleHolder{ private static final Singleton instance = new Singleton(); } public static Singleton getInstance(){ return SingleHolder.instance; } }
5、雙重檢查鎖
特點:是一種使用同步塊加鎖的方法。又稱其為雙重檢查鎖,因為會有兩次檢查instance == null,一次是在同步塊外,一次是在同步快內。為什麽在同步塊內還要檢驗一次,因為可能會有多個線程一起進入同步塊外的if,如果在同步塊內不進行二次檢驗 的話就會生成多個實例了
public class Singleton { private static Singleton instance; //雙重鎖 public static Singleton getInstance() { if(null == instance) { synchronized(Singleton.class){ if(null == instance) { instance = new Singleton(); } return instance; } } return instance; } }
問題:這樣的寫法在很多平臺和優化編譯器上是錯誤的。
原因在於:instance = new Singleton ()並非是原子操作,事實上在JVM中這句話做了三件事:
1.instance = 給新的實體分配內存
2.調用Singleton的構造函數來初始化instance的成員變量
3.將instance對象指向分配的空間(執行完這一步instance就為null)
現在想象一下有線程A和B在調用getInstance,線程A先進入,在執行到步驟1的時候被踢出了cpu。然後線程B進入,B看到的是instance 已經不是null了(內存已經分配),於是它開始放心地使用instance,但這個是錯誤的,因為在這一時刻,instance的成員變量還都是缺省值,A還沒有來得及執行步驟2來完成instance的初始化。
當然編譯器也可以這樣實現:
1. temp = 分配內存
2. 調用temp的構造函數
3. instance = temp
如果編譯器的行為是這樣的話我們似乎就沒有問題了,但事實卻不是那麽簡單,因為我們無法知道某個編譯器具體是怎麽做的,因為在Java的memory model裏對這個問題沒有定義。
雙檢鎖對於基礎類型(比如int)適用。很顯然吧,因為基礎類型沒有調用構造函數這一步。
6、枚舉
特點:通過EasySingleton.INSTANCE來訪問實例,這比調用getInstance()方法簡單多了。創建枚舉默認就是線程安全,而且還能防止反序列化導致重新創建新的對象
public class Singleton { public enum EasySingleton{ INSTANCE; } }
單例模式幾種實現方式