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

Android 設計模式 之 單例模式

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


Singleton模式可以是很簡單的,它的全部只需要一個類就可以完成(看看這章可憐的UML圖)。但是如果在“物件建立的次數以及何時被建立”這兩點上較真起來,Singleton模式可以相當的複雜,比頭五種模式加起來還複雜,譬如涉及到DCL雙鎖檢測(double checked locking)的討論、涉及到多個類載入器(ClassLoader)協同時、涉及到跨JVM(叢集、遠端EJB等)時、涉及到單例物件被銷燬後重建等。

目的:

希望物件只建立一個例項,並且提供一個全域性的訪問點。

圖6.1 單例模式的UML圖

結構是簡單的,但是卻存在一下情況;

1.每次從getInstance()都能返回一個且唯一的一個物件。

2.資源共享情況下,getInstance()必須適應多執行緒併發訪問。

3.提高訪問效能。

4.懶載入(Lazy Load),在需要的時候才被構造。


首先實現1中的單例模式A:

[java]  view plain copy
  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:

[java]  view plain copy
  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:

[java]  view plain copy
  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:

[java]  view plain copy
  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:

[java]  view plain copy
  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.}