1、單例模式常見情景
設計模式中,最簡單不過的就是單例模式。先看看單例模式
原文:http://www.iteye.com/topic/575052
Singleton模式可以是很簡單的,它的全部只需要一個類就可以完成(看看這章可憐的UML圖)。但是如果在“對象建立的次數以及何時被建立”這兩點上較真起來,Singleton模式可以相當的複雜,比頭五種模式加起來還複雜,譬如涉及到DCL雙鎖檢測(double checked locking)的討論、涉及到多個類載入器(ClassLoader)協同時、涉及到跨JVM(叢集、遠程EJB等)時、涉及到單例對象被銷毀後重建等。
目的:
希望對象只建立一個執行個體,並且提供一個全域的訪問點。
圖6.1 單例模式的UML圖
結構是簡單的,但是卻存在一下情況;
1.每次從getInstance()都能返回一個且唯一的一個對象。
2.資源共用情況下,getInstance()必須適應多線程並發訪問。
3.提高訪問效能。
4.懶載入(Lazy Load),在需要的時候才被構造。
首先實現1中的單例模式A:
4.public class SingletonA { 5. 6. /** 7. * 單例對象執行個體 8. */ 9. private static SingletonA instance = null; 10. 11. public static SingletonA getInstance() { 12. if (instance == null) { //line 12 13. instance = new SingletonA(); //line 13 14. } 15. return instance; 16. } 17.}
這個寫法我們把四點需求從上往下檢測,發現第2點的時候就出了問題,假設這樣的情境:兩個線程並發調用Singleton.getInstance(),假設線程一先判斷完instance是否為null,既代碼中的line 12進入到line 13的位置。剛剛判斷完畢後,JVM將CPU資源切換給線程二,由於線程一還沒執行line 13,所以instance仍然是空的,因此線程二執行了new Signleton()操作。片刻之後,線程一被重新喚醒,它執行的仍然是new
Signleton()操作。所以這種設計的單例模式不能滿足第2點需求。
下面我們繼續
實現2中單例模式B:
4.public class SingletonB { 5. 6. /** 7. * 單例對象執行個體 8. */ 9. private static SingletonB instance = null; 10. 11. public synchronized static SingletonB getInstance() { 12. if (instance == null) { 13. instance = new SingletonB(); 14. } 15. return instance; 16. } 17.}
比起單例A僅僅在方法中多了一個synchronized修飾符,現在可以保證不會出線程問題了。但是這裡有個很大(至少耗時比例上很大)的效能問題。除了第一次調用時是執行了SingletonKerriganB的建構函式之外,以後的每一次調用都是直接返回instance對象。返回對象這個操作耗時是很小的,絕大部分的耗時都用在synchronized修飾符的同步準備上,因此從效能上說很不划算。
實現3單例模式C:
4.public class SingletonC { 5. 6. /** 7. * 單例對象執行個體 8. */ 9. private static SingletonKerriganD instance = null; 10. 11. public static SingletonC getInstance() { 12. if (instance == null) { 13. synchronized (SingletonC.class) { 14. if (instance == null) { 15. instance = new SingletonC(); 16. } 17. } 18. } 19. return instance; 20. } 21.}
看起來這樣已經達到了我們的要求,除了第一次建立對象之外,其他的訪問在第一個if中就返回了,因此不會走到同步塊中。已經完美了嗎?
我們來看看這個情境:假設線程一執行到instance = new SingletonKerriganD()這句,這裡看起來是一句話,但實際上它並不是一個原子操作(原子操作的意思就是這條語句要麼就被執行完,要麼就沒有被執行過,不能出現執行了一半這種情形)。事實上進階語言裡面非原子操作有很多,我們只要看看這句話被編譯後在JVM執行的對應彙編代碼就發現,這句話被編譯成8條彙編指令,大致做了3件事情:
1.給Kerrigan的執行個體分配記憶體。
2.初始化Kerrigan的構造器
3.將instance對象指向分配的記憶體空間(注意到這步instance就非null了)。
但是,由於Java編譯器允許處理器亂序執行(out-of-order),以及JDK1.5之前JMM(Java Memory Medel)中Cache、寄存器到主記憶體回寫順序的規定,上面的第二點和第三點的順序是無法保證的,也就是說,執行順序可能是1-2-3也可能是1-3-2,如果是後者,並且在3執行完畢、2未執行之前,被切換到線程二上,這時候instance因為已經線上程一內執行過了第三點,instance已經是非空了,所以線程二直接拿走instance,然後使用,然後順理成章地報錯,而且這種難以跟蹤難以重現的錯誤估計調試上一星期都未必能找得出來,真是一茶几的杯具啊。
DCL的寫法來實現單例是很多技術書、教科書(包括基於JDK1.4以前版本的書籍)上推薦的寫法,實際上是不完全正確的。的確在一些語言(譬如C語言)上DCL是可行的,取決於是否能保證2、3步的順序。在JDK1.5之後,官方已經注意到這種問題,因此調整了JMM、具體化了volatile關鍵字,因此如果JDK是1.5或之後的版本,只需要將instance的定義改成“private volatile static SingletonKerriganD
instance = null;”就可以保證每次都去instance都從主記憶體讀取,就可以使用DCL的寫法來完成單例模式。當然volatile或多或少也會影響到效能,最重要的是我們還要考慮JDK1.42以及之前的版本,所以本文中單例模式寫法的改進還在繼續。
代碼倒越來越複雜了,現在先來個返璞歸真,根據JLS(Java Language Specification)中的規定,一個類在一個ClassLoader中只會被初始化一次,這點是JVM本身保證的,那就把初始化執行個體的事情扔給JVM好了.
實現4單例模式D:
4.public class SingletonD { 5. 6. /** 7. * 單例對象執行個體 8. */ 9. private static SingletonD instance = new SingletonD(); 10. 11. public static SingletonD getInstance() { 12. return instance; 13. } 14.}
這種寫法不會出現並發問題,但是它是餓漢式的,在ClassLoader載入類後Kerrigan的執行個體就會第一時間被建立,餓漢式的建立方式在一些情境中將無法使用:譬如執行個體的建立是依賴參數或者設定檔的,在getInstance()之前必須調用某個方法設定參數給它,那樣這種單例寫法就無法使用了。
可帶參數單例模式E:
4.public class SingletonE { 5. 6. private static class SingletonHolder { 7. /** 8. * 單例對象執行個體 9. */ 10. static final SingletonE INSTANCE = new SingletonE(); 11. } 12. 13. public static SingletonE getInstance() { 14. return SingletonHolder.INSTANCE; 15. } 16.}
這種寫法仍然使用JVM本身機制保證了安全執行緒問題;由於SingletonHolder是私人的,除了getInstance()之外沒有辦法訪問它,因此它是懶漢式的;同時讀取執行個體的時候不會進行同步,沒有效能缺陷;也不依賴JDK版本。
當然,使用者以其它方式構造單例的對象,如果設計者不希望這樣的情況發生,則需要做規避措施。其它途徑建立單例執行個體的方式有:
1.直接new單例對象
2.通過反射構造單例對象
3.通過序列化構造單例對象。
對於第一種情況,一般我們會加入一個private或者protected的建構函式,這樣系統就不會自動添加那個public的建構函式了,因此只能調用裡面的static方法,無法通過new建立對象。
對於第二種情況,反射時可以使用setAccessible方法來突破private的限制,我們需要做到第一點工作的同時,還需要在在 ReflectPermission("suppressAccessChecks") 許可權下使用安全管理器(SecurityManager)的checkPermission方法來限制這種突破。一般來說,不會真的去做這些事情,都是通過應用伺服器進行後台配置實現。
對於第三種情況,如果單例對象有必要實現Serializable介面(很少出現),則應當同時實現readResolve()方法來保證還原序列化的時候得到原來的對象。
終極版單例模式F:
4.public class SingletonF implements Serializable { 5. 6. private static class SingletonHolder { 7. /** 8. * 單例對象執行個體 9. */ 10. static final SingletonF INSTANCE = new SingletonF(); 11. } 12. 13. public static SingletonF getInstance() { 14. return SingletonHolder.INSTANCE; 15. } 16. 17. /** 18. * private的建構函式用於避免外界直接使用new來執行個體化對象 19. */ 20. private SingletonF() { 21. } 22. 23. /** 24. * readResolve方法應對單例對象被序列化時候 25. */ 26. private Object readResolve() { 27. return getInstance(); 28. } 29.}
2、android中源碼單例模式舉例
1、日曆模組
App路徑:packages/providers/CalendarProvider
檔案:packages/providers/CalendarProvider/src/com/android/provider/calendar/CalendarDatabaseHelper.java
單例代碼:
private static CalendarDatabaseHelper sSingleton = null; public static synchronized CalendarDatabaseHelper getInstance(Context context) { if (sSingleton == null) { sSingleton = new CalendarDatabaseHelper(context); } return sSingleton; }
可以看出,這是用到了2中的單例模式B.
2.Collator類
檔案:libcore/luni/src/main/java/com/ibm/icu4jni/text/Callator.java
libcore/luni/src/main/java/com/ibm/icu4jni/text/RuleBasedCallator.java
單例代碼:
public static Collator getInstance(Locale locale) { return new RuleBasedCollator(locale); }
RuleBasedCollator(Locale locale) { m_collator_ = NativeCollation.openCollator(locale.toString()); }
static native int openCollator(String locale);
這就是上面給出的單例模式E,可帶參數的單例模式
3.Editable類
檔案:frameworks/base/core/java/android/text/Editable.java
private static Editable.Factory sInstance = new Editable.Factory(); /** * Returns the standard Editable Factory. */ public static Editable.Factory getInstance() { return sInstance; }
可見這是單例模式D是執行個體應用
4.AccessibilityManager類
檔案:frameworks/base/core/java/android/view/accessibility/AccessibilityManager.java
public static AccessibilityManager getInstance(Context context) { synchronized (sInstanceSync) { if (sInstance == null) { sInstance = new AccessibilityManager(context); } } return sInstance; }
這是單例模式C的應用。
android使用單例模式的地方很多,特別是資料庫建立時,就會使用到單例模式。因每種單例模式試用情境不一樣,所以android在不同地方使用了不同的單例模式實現方式。