java/android 設計模式學習筆記(1)--- 單例模式

Shawn_Dut發表於2016-11-24

  前段時間公司一些同事在討論單例模式(我是最渣的一個,都插不上嘴 T__T ),這個模式使用的頻率很高,也可能是很多人最熟悉的設計模式,當然單例模式也算是最簡單的設計模式之一吧,簡單歸簡單,但是在實際使用的時候也會有一些坑。
  PS:對技術感興趣的同鞋加群544645972一起交流。

設計模式總目錄

  java/android 設計模式學習筆記目錄

特點

  確保某一個類只有一個例項,而且自行例項化並向整個系統提供這個例項。

  單例模式的使用很廣泛,比如:執行緒池(threadpool)、快取(cache)、對話方塊、處理偏好設定、和登錄檔(registry)的物件、日誌物件,充當印表機、顯示卡等裝置的驅動程式的物件等,這些類的物件只能有一個例項,如果製造出多個例項,就會導致很多問題的產生,程式的行為異常,資源使用過量,或者不一致的結果等,所以單例模式最主要的特點:

  1. 建構函式不對外開放,一般為private;
  2. 通過一個靜態方法或者列舉返回單例類物件;
  3. 確保單例類的物件有且只有一個,尤其是在多執行緒的環境下;
  4. 確保單例類物件在反序列化時不會重新構建物件。
通過將單例類建構函式私有化,使得客戶端不能通過 new 的形式手動構造單例類的物件。單例類會暴露一個共有靜態方法,客戶端需要呼叫這個靜態方法獲取到單例類的唯一物件,在獲取到這個單例物件的過程中需要確保執行緒安全,即在多執行緒環境下構造單例類的物件也是有且只有一個,這是單例模式較關鍵的一個地方。
  • 主要優點
  • 單例模式的主要優點如下:
  1. 單例模式提供了對唯一例項的受控訪問。因為單例類封裝了它的唯一例項,所以它可以嚴格控制客戶怎樣以及何時訪問它。
  2. 由於在系統記憶體中只存在一個物件,因此可以節約系統資源,對於一些需要頻繁建立和銷燬的物件單例模式無疑可以提高系統的效能。
  3. 允許可變數目的例項。基於單例模式我們可以進行擴充套件,使用與單例控制相似的方法來獲得指定個數的物件例項,既節省系統資源,又解決了單例物件共享過多有損效能的問題。
  • 主要缺點
    1. 由於單例模式中沒有抽象層,因此單例類的擴充套件有很大的困難。
    2. 單例類的職責過重,在一定程度上違背了“單一職責原則”。因為單例類既充當了工廠角色,提供了工廠方法,同時又充當了產品角色,包含一些業務方法,將產品的建立和產品的本身的功能融合到一起。
    3. 現在很多物件導向語言(如Java、C#)的執行環境都提供了自動垃圾回收的技術,因此,如果例項化的共享物件長時間不被利用,系統會認為它是垃圾,會自動銷燬並回收資源,下次利用時又將重新例項化,這將導致共享的單例物件狀態的丟失。
    4. 單例物件如果持有Context,那麼很容易引發記憶體洩漏,此時需要注意傳遞給單例物件的Context最好是 Application Context。

    UML類圖

    這裡寫圖片描述

      類圖很簡單,Singleton 類有一個 static 的 instance物件,型別為 Singleton ,建構函式為 private,提供一個 getInstance() 的靜態函式,返回剛才的 instance 物件,在該函式中進行初始化操作。

    示例與原始碼

      單例模式的寫法很多,總結一下:

    lazy initialization, thread-unsafety(懶漢法,執行緒不安全)

      延遲初始化,一般很多人稱為懶漢法,寫法一目瞭然,在需要使用的時候去呼叫getInstance()函式去獲取Singleton的唯一靜態物件,如果為空,就會去做一個額外的初始化操作。

    public class Singleton {
        private static Singleton instance = null;
        private Singleton(){}
        public static Singleton getInstance() {
            if(instance == null) 
                instance = new Singleton();
            return instance;
        }
    }複製程式碼

      需要注意的是這種寫法在多執行緒操作中是不安全的,後果是可能會產生多個Singleton物件,比如兩個執行緒同時執行getInstance()函式時,然後同時執行到 new 操作時,最後很有可能會建立兩個不同的物件。

    lazy initialization, thread-safety, double-checked(懶漢法,執行緒安全)

      需要做到執行緒安全,就需要確保任意時刻只能有且僅有一個執行緒能夠執行new Singleton物件的操作,所以可以在getInstance()函式上加上 synchronized 關鍵字,類似於:

    public static synchronized Singleton getInstance() {
            if(singleton == null) 
                instance = new Singleton();
            return instance;
        }複製程式碼

    但是套用《Head First》上的一句話,對於絕大部分不需要同步的情況來說,synchronized 會讓函式執行效率糟糕一百倍以上(Since synchronizing a method could in some extreme cases decrease performance by a factor of 100 or higher),所以就有了double-checked(雙重檢測)的方法:

    public class Singleton {
        private volatile static Singleton instance = null;
        private Singleton(){}
        public static Singleton getInstance() {
            if (instance == null){
                synchronized (Singleton.class){
                    if (instance == null){
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }複製程式碼

      我們假設兩個執行緒A,B同時執行到了getInstance()這個方法,第一個if判斷,兩個執行緒同時為true,進入if語句,裡面有個 synchronized 同步,所以之後有且僅有一個執行緒A會執行到 synchronized 語句內部,接著再次判斷instance是否為空,為空就去new Singleton物件並且賦值給instance,A執行緒退出 synchronized 語句,交出同步鎖,B執行緒進入 synchronized 語句內部,if判斷instance是否為空,防止建立不同的instance物件,這也是第二個if判斷的作用,B執行緒發現不為空,所以直接退出,所以最終A和B執行緒可以獲取到同一個Singleton物件,之後的執行緒呼叫getInstance()函式,都會因為Instance不為空而直接返回,不會受到 synchronized 的效能影響。

    volatile關鍵字介紹

      double-checked方法用到了volatile關鍵字,volatile關鍵字的作用需要仔細介紹一下,在C/C++中,volatile關鍵字的作用和java中是不一樣的,總結一下:

    1. C/C++中的volatile關鍵字作用
    • 可見性
    • “可見性”指的是在一個執行緒中對該變數的修改會馬上由工作記憶體(Work Memory)寫回主記憶體(Main Memory),所以會馬上反應在其它執行緒的讀取操作中。順便一提,工作記憶體和主記憶體可以近似理解為實際電腦中的快取記憶體和主存,工作記憶體是執行緒獨享的,主存是執行緒共享的。
    • 不可優化性
    • “不可優化”特性,volatile告訴編譯器,不要對我這個變數進行各種激進的優化,甚至將變數直接消除,保證程式設計師寫在程式碼中的指令,一定會被執行。
    • 順序性
    • ”順序性”,能夠保證Volatile變數間的順序性,編譯器不會進行亂序優化。Volatile變數與非Volatile變數的順序,編譯器不保證順序,可能會進行亂序優化。同時,C/C++ Volatile關鍵詞,並不能用於構建happens-before語義,因此在進行多執行緒程式設計時,要小心使用volatile,不要掉入volatile變數的使用陷阱之中。
  • java中volatile關鍵字作用
  • Java也支援volatile關鍵字,但它被用於其他不同的用途。當volatile用於一個作用域時,Java保證如下:
    • (適用於Java所有版本)讀和寫一個volatile變數有全域性的排序。也就是說每個執行緒訪問一個volatile作用域時會在繼續執行之前讀取它的當前值,而不是(可能)使用一個快取的值。(但是並不保證經常讀寫volatile作用域時讀和寫的相對順序,也就是說通常這並不是有用的執行緒構建)。
    • (適用於Java5及其之後的版本)volatile的讀和寫建立了一個happens-before關係,類似於申請和釋放一個互斥鎖[8]。
    使用volatile會比使用鎖更快,但是在一些情況下它不能工作。volatile使用範圍在Java5中得到了擴充套件,特別是雙重檢查鎖定現在能夠正確工作[9]。

    上面有一個細節,java 5版本之後volatile的讀與寫才建立了一個happens-before的關係,之前的版本會出現一個問題:Why is volatile used in this example of double checked locking,這個答案寫的很清楚了,執行緒 A 在完全構造完 instance 物件之前就會給 instance 分配記憶體,執行緒B在看到 instance 已經分配了記憶體不為空就回去使用它,所以這就造成了B執行緒使用了部分初始化的 instance 物件,最後就會出問題了。Double-checked locking裡面有一句話

    As of J2SE 5.0, this problem has been fixed. The volatile keyword now ensures that 
    multiple threads handle the singleton instance correctly. This new idiom is 
    described in [2] and [3].複製程式碼

    所以對於 android 來說,使用 volatile關鍵字是一點問題都沒有的了。

      參考文章

      Volatile變數

      C/C++ Volatile關鍵詞深度剖析

      Java中volatile的作用以及用法

    eager initialization thread-safety (餓漢法,執行緒安全)

      “餓漢法”就是在使用該變數之前就將該變數進行初始化,這當然也就是執行緒安全的了,寫法也很簡單:

    private static Singleton instance = new Singleton();
    private Singleton(){
        name = "eager initialization thread-safety  1";
    }
    
    public static Singleton getInstance(){
        return instance;
    }複製程式碼

    或者

    private static Singleton instance  = null;
    private Singleton(){
        name = "eager initialization thread-safety  2";
    }
    
    static {
        instance = new Singleton();
    }
    public Singleton getInstance(){
        return instance;
    }複製程式碼

    程式碼都很簡單,一個是直接進行初始化,另一個是使用靜態塊進行初始化,目的都是一個:在該類進行載入的時候就會初始化該物件,而不管是否需要該物件。這麼寫的好處是編寫簡單,而且是執行緒安全的,但是這時候初始化instance顯然沒有達到lazy loading的效果。

    static inner class thread-safety (靜態內部類,執行緒安全)

      由於在java中,靜態內部類是在使用中初始化的,所以可以利用這個天生的延遲載入特性,去實現一個簡單,延遲載入,執行緒安全的單例模式:

    private static class SingletonHolder{
        private static final Singleton instance = new Singleton();
    }
    private Singleton(){
        name = "static inner class thread-safety";
    }
    
    public static Singleton getInstance(){
        return SingletonHolder.instance;
    }複製程式碼

    定義一個 SingletonHolder 的靜態內部類,在該類中定義一個外部類 Singleton 的靜態物件,並且直接初始化,在外部類 Singleton 的 getInstance() 方法中直接返回該物件。由於靜態內部類的使用是延遲載入機制,所以只有當執行緒呼叫到 getInstance() 方法時才會去載入 SingletonHolder 類,載入這個類的時候又會去初始化 instance 變數,所以這個就實現了延遲載入機制,同時也只會初始化這一次,所以也是執行緒安全的,寫法也很簡單。

      

    PS

      上面提到的所有實現方式都有兩個共同的缺點:


    • 都需要額外的工作(Serializable、transient、readResolve())來實現序列化,否則每次反序列化一個序列化的物件例項時都會建立一個新的例項。

    • 可能會有人使用反射強行呼叫我們的私有構造器(如果要避免這種情況,可以修改構造器,讓它在建立第二個例項的時候拋異常)。

    enum (列舉寫法)

      JDK1.5 之後加入 enum 特性,可以使用 enum 來實現單例模式:

    enum SingleEnum{
        INSTANCE("enum singleton thread-safety");
    
        private String name;
    
        SingleEnum(String name){
            this.name = name;
        }
    
        public String getName(){
            return name;
        }
    }複製程式碼

    使用列舉除了執行緒安全和防止反射強行呼叫構造器之外,還提供了自動序列化機制,防止反序列化的時候建立新的物件。因此,Effective Java推薦儘可能地使用列舉來實現單例。但是很不幸的是 android 中並不推薦使用 enum ,主要是因為在 java 中列舉都是繼承自 java.lang.Enum 類,首次呼叫時,這個類會呼叫初始化方法來準備每個列舉變數。每個列舉項都會被宣告成一個靜態變數,並被賦值。在實際使用時會有點問題,這是 google 的官方文件介紹:

    Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android

    這篇部落格也專門計算了 enum 的大小:胡凱-The price of ENUMs,所以列舉寫法的缺點也就很明顯了。

    登記式

       登記式單例實際上維護了一組單例類的例項,將這些例項存放在一個Map(登記薄)中,對於已經登記過的例項,則從Map直接返回,對於沒有登記的,則先登記,然後返回。

    //類似Spring裡面的方法,將類名註冊,下次從裡面直接獲取。  
    public class Singleton {  
        private static Map map = new HashMap();  
        static{  
            Singleton single = new Singleton();  
            map.put(single.getClass().getName(), single);  
        }  
        //保護的預設構造子  
        protected Singleton(){}  
        //靜態工廠方法,返還此類惟一的例項  
        public static Singleton getInstance(String name) {  
            if(name == null) {  
                name = Singleton.class.getName();  
                System.out.println("name == null"+"--->name="+name);  
            }  
            if(map.get(name) == null) {  
                try {  
                    map.put(name, (Singleton) Class.forName(name).newInstance());  
                } catch (InstantiationException e) {  
                    e.printStackTrace();  
                } catch (IllegalAccessException e) {  
                    e.printStackTrace();  
                } catch (ClassNotFoundException e) {  
                    e.printStackTrace();  
                }  
            }  
            return map.get(name);  
        }  
        //一個示意性的商業方法  
        public String about() {      
            return "Hello, I am RegSingleton.";      
        }      
        public static void main(String[] args) {  
            Singleton single3 = Singleton.getInstance(null);  
            System.out.println(single3.about());  
        }  
    }  複製程式碼

    這種方式我極少見到,另外其實內部實現還是用的餓漢式單例,因為其中的static方法塊,它的單例在類被裝載的時候就被例項化了。

    總結

      綜上所述,平時在 android 中使用 double-checked 或者 SingletonHolder 都是可以的,畢竟 android 早就不使用 JDK5 之前的版本了。由於 android 中的多程式機制,在不同程式中無法建立同一個 instance 變數,就像 Application 類會初始化兩次一樣,這點需要注意。

      但是不管採取何種方案,請時刻牢記單例的三大要點:

    • 執行緒安全;
    • 延遲載入;
    • 序列化與反序列化安全。
      單例模式同時也有缺點:
    • 單例模式一般沒有介面,擴充套件很困難,若要擴充套件,除了修改程式碼基本上沒有第二種途徑可以實現;
    • 單例物件如果持有 Context,那麼很容易引發記憶體洩漏,此時需要注意傳遞給單例物件的 Context 最好為 Application Context。

    建立型模式 Rules of thumb

      有些時候建立型模式是可以重疊使用的,有一些抽象工廠模式原型模式都可以使用的場景,這個時候使用任一設計模式都是合理的;在其他情況下,他們各自作為彼此的補充:抽象工廠模式可能會使用一些原型類來克隆並且返回產品物件。

      抽象工廠模式建造者模式原型模式都能使用單例模式來實現他們自己;抽象工廠模式經常也是通過工廠方法模式實現的,但是他們都能夠使用原型模式來實現;

      通常情況下,設計模式剛開始會使用工廠方法模式(結構清晰,更容易定製化,子類的數量爆炸),如果設計者發現需要更多的靈活性時,就會慢慢地發展為抽象工廠模式原型模式或者建造者模式(結構更加複雜,使用靈活);

      原型模式並不一定需要繼承,但是它確實需要一個初始化的操作,工廠方法模式一定需要繼承,但是不一定需要初始化操作;

      使用裝飾者模式或者組合模式的情況通常也可以使用原型模式來獲得益處;

      單例模式中,只要將構造方法的訪問許可權設定為 private 型,就可以實現單例。但是原型模式的 clone 方法直接無視構造方法的許可權來生成新的物件,所以,單例模式原型模式是衝突的,在使用時要特別注意。

    原始碼下載

      github.com/zhaozepeng/…

    引用


    www.tekbroaden.com/singleton-j…

    hedengcheng.com/?p=725

    www.cnblogs.com/hxsyl/archi…

    www.blogjava.net/kenzhh/arch…

    blog.csdn.net/jason0539/a…

    sourcemaking.com/design_patt…

    stackoverflow.com/questions/7…

    stackoverflow.com/questions/1…

    en.wikipedia.org/wiki/Single…

    en.wikipedia.org/wiki/Double…

    zh.wikipedia.org/wiki/Volati…

    jeremymanson.blogspot.com/2008/11/wha…

    www.jianshu.com/p/d8bf5d08a…

    preshing.com/20130702/th…

    blog.csdn.net/imzoer/arti…


    相關文章