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

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

設計模式總目錄

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

特色

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

  單例模式的使用很普遍,好比:線程池(threadpool)、緩存(cache)、對話框、處理偏好設置、和註冊表(registry)的對象、日誌對象,充當打印機、顯卡等設備的驅動程序的對象等,這些類的對象只能有一個實例,若是製造出多個實例,就會致使不少問題的產生,程序的行爲異常,資源使用過量,或者不一致的結果等,因此單例模式最主要的特色:android

  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 對象,在該函數中進行初始化操做。git

示例與源碼

  單例模式的寫法不少,總結一下:程序員

lazy initialization, thread-unsafety(懶漢法,線程不安全)

  延遲初始化,通常不少人稱爲懶漢法,寫法一目瞭然,在須要使用的時候去調用getInstance()函數去獲取Singleton的惟一靜態對象,若是爲空,就會去作一個額外的初始化操做。github

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變量的使用陷阱之中。
  2. java中volatile關鍵字做用
  3. 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…

相關文章
相關標籤/搜索