| 01 單例模式 |
單例模式,只有一個實例存在于整個JVM中,保證只有一個實例,并可以被外界訪問。它是一種常用的設計模式之一。實現單例模式
的方法有很多種,然而需要考慮包括線程安全在內的一些因素。以下列舉了幾種典型的實現方法。
| 02 實現及問題 |
方法一:懶漢式實現
【懶漢式】私有化構造函數,創建靜態方法,提供單例引用,延遲加載。 重大缺陷:線程不安全,線程A希望能夠使用Singleton
實例,于是第一次調用靜態方法getInstance(),發現此時singleton==null,準備創建實例,突然CPU發生時間片切換,線程B也希望
調用該實例,此時A并未創建,因此singleton==null,B創建Singleton實例,執行完成后,線程A繼續執行,因為已經判斷過singleto
n==null,所以也創建了一個實例對象。 最終導致單例失敗。
1 /** 2 * 懶漢式 寫法1 3 * @author Administrator 4 * 5 */ 6 public class Singleton { 7 PRivate Singleton(){ 8 9 }10 private static Singleton singleton=null;11 //靜態工廠方法12 public static Singleton getInstance(){13 if(singleton==null){14 singleton=new Singleton();15 }16 return singleton;17 }18 }
方法二:餓漢式實現
【餓漢式】懶漢式存在線程安全問題,無法在多線程下實現單例。餓漢式的方法就是可以在類加載時創建一個靜態實例。其缺點在
于類的初始化開銷大。然而卻是天生安全的。
1 /** 2 * 餓漢式:類初始化時,實例化。 3 * @author Administrator 4 * 5 */ 6 public class Singleton3 { 7 private Singleton3(){ 8 9 }10 private static final Singleton3 singleton=new Singleton3();11 public static Singleton3 getInstance(){12 return singleton;13 }14 }
方法三:同步懶漢式1
【synchronized】在靜態方法getInstance()前添加synchronized同步關鍵字實現方法同步, 重大缺點:執行效率低。每一次調
用getInstance()都需要加鎖,而加鎖是非常耗時的,應盡量避免。
1 /** 2 * 同步懶漢式1,添加synchronized 3 * @author Administrator 4 * 5 */ 6 public class Singleton1 { 7 private Singleton1(){ 8 9 }10 private static Singleton1 singleton=null;11 //靜態工廠方法12 public static synchronized Singleton1 getInstance(){13 if(singleton==null){14 singleton=new Singleton1();15 }16 return singleton;17 }18 }
方法四:雙重檢測懶漢式
雙重檢測:即在同步塊前后分別判斷實例是否不為空。可以減少同步數。只有當instance==null的時候,才需要進入同步塊,進行
加鎖操作。當已經創建后,則無需進入同步塊,即不需要加鎖操作,只有第一次需要創建實例對象時,多個線程才需要進入同步塊,節
省了時間。
1 /** 2 * 雙重檢測懶漢式:雙重檢測,提高效率 3 * @author Administrator 4 * 5 */ 6 public class Singleton2 { 7 private Singleton2(){ 8 9 }10 private static volatile Singleton2 singleton=null;11 //靜態工廠方法12 public static Singleton2 getInstance(){13 if(singleton==null){ //若該線程檢測到不為空,則不用創建,否則,進入同步塊14 synchronized(Singleton2.class){15 if(singleton==null){ //多個線程開始同步,若一個線程創建成功,則其它后進入的線程都不用創建16 singleton=new Singleton2();17 }18 }19 20 }21 return singleton;22 }23 }
在雙重檢測中,使用了 volatile 關鍵字,能夠保證訪問到的變量是最后修改的。為什么要加volatile關鍵字呢?因為在java中雙重
檢測很可能導致失敗的結果,得到一個存在但未初始化完成的實例。《Java與模式》的總結是:在Java編譯器中,Singleton類的初
始化與instance變量賦值的順序不可預料。如果一個線程在沒有同步化的條件下讀取instance引用,并調用這個對象的方法的
話,可能會發現對象初始化過程尚未完成,從而造成崩潰。
其原因在于 Java虛擬機中實現的 無序寫入 問題。所謂無序寫入,JVM編譯器能用不同的順序從程序的解釋中生成指令,并且存儲
變量在寄存器中,而并非內存,處理器可以并行或者顛倒次序的執行指令。完成的結果與嚴格連續執行的結果一致。例如在雙重檢測中
singleton=new Singleton() 語句可能包含下列偽代碼表示的指令。
----------------------------------------------------------
memory=allocate(); //1、分配內存
memory=Singleton(); //2、初始化實例
singleton=memory; //3、將實例賦值給引用變量。
----------------------------------------------------------
由于JVM中存在無序寫入 問題,那么很可能先執行語句3,再執行語句2。即如下所以:
----------------------------------------------------------
memory=allocate(); //1、分配內存
singleton=memory(); //2、將未初始化的實例復制給引用變量
memory=Singleton(); //3、實例初始化
----------------------------------------------------------
如果在執行語句2后,線程切換成另外一個線程,此時線程可知 instance!=null,然而此時該實例卻是未初始化的。詳細解釋
可以參考文獻[1]。
那么是否加了volatile關鍵字 雙重檢測就達到最優了呢?不是,因為volatile與synchronized有相當的含義,訪問變量時
效率同synchronized。且由于兩次檢測,相比synchronized block 效率更低一些。[1]
方法五: 靜態內部類
使用靜態內部類存放靜態實例對象。在加載Singleton類時,并不加載內部類,在調用getInstance()時調用內部類,靜態實例時,
才加載靜態內部類和內部靜態實例對象。從而達到延時加載的目的。
1 /** 2 * 懶漢式:通過靜態內部類實現方式,既實現了線程安全,又提高了效率 3 * @author Administrator 4 * 5 */ 6 public class Singleton4 { 7 private Singleton4(){ 8 9 }10 public static Singleton4 getInstance(){11 return Instance.singleton; //當需要返回實例的時候,初始化創建靜態實例對象。12 }13 14 private static class Instance{ 15 private static final Singleton4 singleton=new Singleton4();16 }17 }
方法六:Enum枚舉實現
在《Effective Java》中提到,從Java1.5發行版本起,只需編寫一個包含單個元素的枚舉類型,實現Singleton。
1 /** 2 * 枚舉類型實現單例 3 * @author Administrator 4 * 5 */ 6 public class Singleton5 { 7 public enum Singleton{ 8 INSTANCE; 9 10 public void method(){11 //任意方法12 }13 } 14 15 public static void main(String[] args) {16 Singleton.INSTANCE.method();17 }18 }
枚舉方法 簡潔明了,并提供序列化機制,不會在反序列化實例時創建新實例(在其他單例方法中如果要變成可序列化的,不僅要加
上implements Serializable,還需要提供readResolve方法,參加《effective Java》)。也不會受到反射機制的影響,是實現單例
模式的最佳方法。
| 03 梳理概念點 |
反射機制破壞單例模式:通過反射機制 例如借助accessibleObject.setAccessible方法調用私有構造方法。如果要防止這種機制的
影響,需要在構造方法中添加判斷如果創建第二個實例則拋出異常。
| 04 感謝 |
[1] Robin Hu的專欄 http://blog.csdn.net/hudashi/article/details/6949379
[2] cantellow http://cantellow.VEvb.com/blog/838473
[3] junJZ_2008 http://jiangzhengjun.VEvb.com/blog/652440
[4] zhoujy http://my.oschina.net/zhoujy/blog/134958
[5] FinderCheng http://devbean.blog.51cto.com/448512/203501/
[6] 炸斯特 http://blog.csdn.net/jason0539/article/details/23297037
[7] Joshua Bloch 著,楊春花,俞黎敏 譯 《Effective Java中文版》
新聞熱點
疑難解答