1. 程式人生 > >Android設計模式之Singleton單例模式

Android設計模式之Singleton單例模式

轉自 - 

原文1:《Android 設計模式 之 單例模式》http://blog.csdn.net/liguangzhenghi/article/details/8076361

原文0:《主題:探索設計模式之六——單例模式》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.publicclass SingletonA {    
  2. 5.     
  3. 6.    /**  
  4. 7.     * 單例物件例項  
  5. 8.     */
  6. 9.    privatestatic SingletonA instance = null;    
  7. 10.     
  8. 11.    publicstatic 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.publicclass SingletonB {    
  2. 5.     
  3. 6.    /**  
  4. 7.     * 單例物件例項  
  5. 8.     */
  6. 9.    privatestatic SingletonB instance = null;    
  7. 10.     
  8. 11.    publicsynchronizedstatic 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.publicclass SingletonC {    
  2. 5.     
  3. 6.    /**  
  4. 7.     * 單例物件例項  
  5. 8.     */
  6. 9.    privatestatic SingletonKerriganC instance = null;    
  7. 10.     
  8. 11.    publicstatic 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.publicclass SingletonD {    
  2. 5.     
  3. 6.    /**  
  4. 7.     * 單例物件例項  
  5. 8.     */
  6. 9.    privatestatic SingletonD instance = new SingletonD();    
  7. 10.     
  8. 11.    publicstatic SingletonD getInstance() {    
  9. 12.        return instance;    
  10. 13.    }    
  11. 14.}    
這種寫法不會出現併發問題,但是它是餓漢式的,在ClassLoader載入類後Kerrigan的例項就會第一時間被建立,餓漢式的建立方式在一些場景中將無法使用:譬如例項的建立是依賴引數或者配置檔案的,在getInstance()之前必須呼叫某個方法設定引數給它,那樣這種單例寫法就無法使用了。

可帶引數單例模式E:

  1. 4.publicclass SingletonE {    
  2. 5.     
  3. 6.    privatestaticclass SingletonHolder {    
  4. 7.        /**  
  5. 8.         * 單例物件例項  
  6. 9.         */
  7. 10.        staticfinal SingletonE INSTANCE = new SingletonE();    
  8. 11.    }    
  9. 12.     
  10. 13.    publicstatic 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.publicclass SingletonF implements Serializable {    
  2. 5.     
  3. 6.    privatestaticclass SingletonHolder {    
  4. 7.        /**  
  5. 8.         * 單例物件例項  
  6. 9.         */
  7. 10.        staticfinal SingletonF INSTANCE = new SingletonF();    
  8. 11.    }    
  9. 12.     
  10. 13.    publicstatic 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