Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> java/android 設計模式學習筆記(一)---單例模式

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

編輯:關於Android編程

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

特點

  確保某一個類只有一個實例,而且自行實例化並向整個系統提供這個實例。
  單例模式的使用很廣泛,比如:線程池(threadpool)、緩存(cache)、對話框、處理偏好設置、和注冊表(registry)的對象、日志對象,充當打印機、顯卡等設備的驅動程序的對象等,這些類的對象只能有一個實例,如果制造出多個實例,就會導致很多問題的產生,程序的行為異常,資源使用過量,或者不一致的結果等,所以單例模式最主要的特點:

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

 

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中是不一樣的,總結一下:

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關鍵字是一點問題都沒有的了。
 

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
 

登記式

   登記式單例實際上維護了一組單例類的實例,將這些實例存放在一個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 類會初始化兩次一樣,這點需要注意。
  但是不管采取何種方案,請時刻牢記單例的三大要點:

線程安全 延遲加載 序列化與反序列化安全

源碼下載

  https://github.com/zhaozepeng/Design-Patterns

 

 
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved