Android 設計模式 之 單例模式


設計模式中,最簡單不過的就是單例模式。先看看單例模式

原文: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:

  1. 4.public class SingletonA {    
  2. 5.     
  3. 6.    /**  
  4. 7.     * 單例對象實例  
  5. 8.     */    
  6. 9.    private static SingletonA instance = null;    
  7. 10.     
  8. 11.    public static SingletonA getInstance() {    
  9. 12.        if (instance == null) {                              //line 12    
  10. 13.            instance = new SingletonA();          //line 13    
  11. 14.        }    
  12. 15.        return instance;    
  13. 16.    }    
  14. 17.}    

這個寫法我們把四點需求從上往下檢測,發現第2點的時候就出了問題,假設這樣的場景:兩個線程併發調用Singleton.getInstance(),假設線程一先判斷完instance是否爲null,既代碼中的line 12進入到line 13的位置。剛剛判斷完畢後,JVM將CPU資源切換給線程二,由於線程一還沒執行line 13,所以instance仍然是空的,因此線程二執行了new Signleton()操作。片刻之後,線程一被重新喚醒,它執行的仍然是new Signleton()操作。所以這種設計的單例模式不能滿足第2點需求。

下面我們繼續


實現2中單例模式B:

  1. 4.public class SingletonB {    
  2. 5.     
  3. 6.    /**  
  4. 7.     * 單例對象實例  
  5. 8.     */    
  6. 9.    private static SingletonB instance = null;    
  7. 10.     
  8. 11.    public synchronized static SingletonB getInstance() {    
  9. 12.        if (instance == null) {    
  10. 13.            instance = new SingletonB();    
  11. 14.        }    
  12. 15.        return instance;    
  13. 16.    }    
  14. 17.}    

比起單例A僅僅在方法中多了一個synchronized修飾符,現在可以保證不會出線程問題了。但是這裏有個很大(至少耗時比例上很大)的性能問題。除了第一次調用時是執行了SingletonKerriganB的構造函數之外,以後的每一次調用都是直接返回instance對象。返回對象這個操作耗時是很小的,絕大部分的耗時都用在synchronized修飾符的同步準備上,因此從性能上說很不划算。


實現3單例模式C:

  1. 4.public class SingletonC {    
  2. 5.     
  3. 6.    /**  
  4. 7.     * 單例對象實例  
  5. 8.     */    
  6. 9.    private static SingletonKerriganD instance = null;    
  7. 10.     
  8. 11.    public static SingletonC getInstance() {    
  9. 12.        if (instance == null) {    
  10. 13.            synchronized (SingletonC.class) {    
  11. 14.                if (instance == null) {    
  12. 15.                    instance = new SingletonC();    
  13. 16.                }    
  14. 17.            }    
  15. 18.        }    
  16. 19.        return instance;    
  17. 20.    }    
  18. 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:

  1. 4.public class SingletonD {    
  2. 5.     
  3. 6.    /**  
  4. 7.     * 單例對象實例  
  5. 8.     */    
  6. 9.    private static SingletonD instance = new SingletonD();    
  7. 10.     
  8. 11.    public static SingletonD getInstance() {    
  9. 12.        return instance;    
  10. 13.    }    
  11. 14.}    


這種寫法不會出現併發問題,但是它是餓漢式的,在ClassLoader加載類後Kerrigan的實例就會第一時間被創建,餓漢式的創建方式在一些場景中將無法使用:譬如實例的創建是依賴參數或者配置文件的,在getInstance()之前必須調用某個方法設置參數給它,那樣這種單例寫法就無法使用了。

可帶參數單例模式E:

  1. 4.public class SingletonE {    
  2. 5.     
  3. 6.    private static class SingletonHolder {    
  4. 7.        /**  
  5. 8.         * 單例對象實例  
  6. 9.         */    
  7. 10.        static final SingletonE INSTANCE = new SingletonE();    
  8. 11.    }    
  9. 12.     
  10. 13.    public static SingletonE getInstance() {    
  11. 14.        return SingletonHolder.INSTANCE;    
  12. 15.    }    
  13. 16.}    


這種寫法仍然使用JVM本身機制保證了線程安全問題;由於SingletonHolder是私有的,除了getInstance()之外沒有辦法訪問它,因此它是懶漢式的;同時讀取實例的時候不會進行同步,沒有性能缺陷;也不依賴JDK版本。

當然,用戶以其它方式構造單例的對象,如果設計者不希望這樣的情況發生,則需要做規避措施。其它途徑創建單例實例的方式有:

1.直接new單例對象

2.通過反射構造單例對象

3.通過序列化構造單例對象。

對於第一種情況,一般我們會加入一個private或者protected的構造函數,這樣系統就不會自動添加那個public的構造函數了,因此只能調用裏面的static方法,無法通過new創建對象。

對於第二種情況,反射時可以使用setAccessible方法來突破private的限制,我們需要做到第一點工作的同時,還需要在在 ReflectPermission("suppressAccessChecks") 權限下使用安全管理器(SecurityManager)的checkPermission方法來限制這種突破。一般來說,不會真的去做這些事情,都是通過應用服務器進行後臺配置實現。

對於第三種情況,如果單例對象有必要實現Serializable接口(很少出現),則應當同時實現readResolve()方法來保證反序列化的時候得到原來的對象。


終極版單例模式F:

  1. 4.public class SingletonF implements Serializable {    
  2. 5.     
  3. 6.    private static class SingletonHolder {    
  4. 7.        /**  
  5. 8.         * 單例對象實例  
  6. 9.         */    
  7. 10.        static final SingletonF INSTANCE = new SingletonF();    
  8. 11.    }    
  9. 12.     
  10. 13.    public static SingletonF getInstance() {    
  11. 14.        return SingletonHolder.INSTANCE;    
  12. 15.    }    
  13. 16.     
  14. 17.    /**  
  15. 18.     * private的構造函數用於避免外界直接使用new來實例化對象  
  16. 19.     */    
  17. 20.    private SingletonF() {    
  18. 21.    }    
  19. 22.     
  20. 23.    /**  
  21. 24.     * readResolve方法應對單例對象被序列化時候  
  22. 25.     */    
  23. 26.    private Object readResolve() {    
  24. 27.        return getInstance();    
  25. 28.    }    
  26. 29.}    

2、android中源碼單例模式舉例

1、日曆模塊 

App路徑:packages/providers/CalendarProvider

文件:packages/providers/CalendarProvider/src/com/android/provider/calendar/CalendarDatabaseHelper.java

單例代碼:

  1. private static CalendarDatabaseHelper sSingleton = null;      
  2. public static synchronized CalendarDatabaseHelper getInstance(Context context) {  
  3.         if (sSingleton == null) {  
  4.             sSingleton = new CalendarDatabaseHelper(context);  
  5.         }  
  6.         return sSingleton;  
  7.     }  
可以看出,這是用到了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

單例代碼:

  1. public static Collator getInstance(Locale locale) {  
  2.     return new RuleBasedCollator(locale);  
  3. }  
  4. p;     
  1.     RuleBasedCollator(Locale locale) {  
  2.         m_collator_ = NativeCollation.openCollator(locale.toString());  
  3.     }  
  4.       
  1. static native int openCollator(String locale);  

這就是上面給出的單例模式E,可帶參數的單例模式


3.Editable類

文件:frameworks/base/core/java/android/text/Editable.java

  1. private static Editable.Factory sInstance = new Editable.Factory();  
  2.   
  3. /** 
  4.  * Returns the standard Editable Factory. 
  5.  */  
  6. public static Editable.Factory getInstance() {  
  7.     return sInstance;  
  8. }  

可見這是單例模式D是實例應用

4.AccessibilityManager類

文件:frameworks/base/core/java/android/view/accessibility/AccessibilityManager.java

  1. public static AccessibilityManager getInstance(Context context) {  
  2.     synchronized (sInstanceSync) {  
  3.         if (sInstance == null) {  
  4.             sInstance = new AccessibilityManager(context);  
  5.         }  
  6.     }  
  7.     return sInstance;  
  8. }  

這是單例模式C的應用。

android使用單例模式的地方很多,特別是數據庫創建時,就會使用到單例模式。因每種單例模式試用場景不一樣,所以android在不同地方使用了不同的單例模式實現方式。


發佈了34 篇原創文章 · 獲贊 115 · 訪問量 33萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章