設(shè)計模式:單例模式

確保某一個類只有一個實例,而且自行實例化并向整個系統(tǒng)提供這個實例。

單例模式的使用很廣泛,比如:線程池(threadpool)、緩存(cache)、對話框、處理偏好設(shè)置、和注冊表(registry)的對象、日志對象,充當(dāng)打印機、顯卡等設(shè)備的驅(qū)動程序的對象等,這些類的對象只能有一個實例,如果制造出多個實例,就會導(dǎo)致很多問題的產(chǎn)生,程序的行為異常,資源使用過量,或者不一致的結(jié)果等

特點

  • 構(gòu)造函數(shù)不對外開放,一般為private;
  • 通過一個靜態(tài)方法或者枚舉返回單例類對象;
  • 確保單例類的對象有且只有一個,尤其是在多線程的環(huán)境下;
  • 確保單例類對象在反序列化時不會重新構(gòu)建對象。
  • 通過將單例類構(gòu)造函數(shù)私有化,使得客戶端不能通過 new 的形式手動構(gòu)造單例類的對象。單例類會暴露一個共有靜態(tài)方法,客戶端需要調(diào)用這個靜態(tài)方法獲取到單例類的唯一對象,在獲取到這個單例對象的過程中需要確保線程安全,即在多線程環(huán)境下構(gòu)造單例類的對象也是有且只有一個,這是單例模式較關(guān)鍵的一個地方。

主要優(yōu)點

  • 單例模式提供了對唯一實例的受控訪問。因為單例類封裝了它的唯一實例,所以它可以嚴(yán)格控制客戶怎樣以及何時訪問它。
  • 由于在系統(tǒng)內(nèi)存中只存在一個對象,因此可以節(jié)約系統(tǒng)資源,對于一些需要頻繁創(chuàng)建和銷毀的對象單例模式無疑可以提高系統(tǒng)的性能。
  • 允許可變數(shù)目的實例?;趩卫J轿覀兛梢赃M行擴展,使用與單例控制相似的方法來獲得指定個數(shù)的對象實例,既節(jié)省系統(tǒng)資源,又解決了單例對象共享過多有損性能的問題。

主要缺點

  • 由于單例模式中沒有抽象層,因此單例類的擴展有很大的困難。

  • 單例類的職責(zé)過重,在一定程度上違背了“單一職責(zé)原則”。因為單例類既充當(dāng)了工廠角色,提供了工廠方法,同時又充當(dāng)了產(chǎn)品角色,包含一些業(yè)務(wù)方法,將產(chǎn)品的創(chuàng)建和產(chǎn)品的本身的功能融合到一起。

  • 現(xiàn)在很多面向?qū)ο笳Z言(如Java、C#)的運行環(huán)境都提供了自動垃圾回收的技術(shù),因此,如果實例化的共享對象長時間不被利用,系統(tǒng)會認為它是垃圾,會自動銷毀并回收資源,下次利用時又將重新實例化,這將導(dǎo)致共享的單例對象狀態(tài)的丟失。

  • 單例對象如果持有Context,那么很容易引發(fā)內(nèi)存泄漏,此時需要注意傳遞給單例對象的Context最好是 Application Context。

類圖

單例模式

寫法

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

public class Singleton {

    private static Singleton instance = null;

    private Singleton(){}

    public static Singleton getInstance() {

        if(instance == null) 
            instance = new Singleton();//用到的時候才初始化
        return instance;
    }

}

需要注意的是這種寫法在多線程操作中是不安全的,后果是可能會產(chǎn)生多個Singleton對象,比如兩個線程同時執(zhí)行g(shù)etInstance()函數(shù)時,然后同時執(zhí)行到 new 操作時,最后很有可能會創(chuàng)建兩個不同的對象。

lazy initialization, thread-safety, double-checked(懶漢法,線程安全)

需要做到線程安全,就需要確保任意時刻只能有且僅有一個線程能夠執(zhí)行new Singleton對象的操作,所以可以在getInstance()函數(shù)上加上 synchronized 關(guān)鍵字,類似于:

public static synchronized Singleton getInstance() {
    if(singleton == null) 
        instance = new Singleton();
    return instance;
}

double-checked(雙重檢測)

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

//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;
 }
}

我們假設(shè)兩個線程A,B同時執(zhí)行到了getInstance()這個方法,第一個if判斷,兩個線程同時為true,進入if語句,里面有個 synchronized 同步,所以之后有且僅有一個線程A會執(zhí)行到 synchronized 語句內(nèi)部,接著再次判斷instance是否為空,為空就去new Singleton對象并且賦值給instance,A線程退出 synchronized 語句,交出同步鎖,B線程進入 synchronized 語句內(nèi)部,if判斷instance是否為空,防止創(chuàng)建不同的instance對象,這也是第二個if判斷的作用,B線程發(fā)現(xiàn)不為空,所以直接退出,所以最終A和B線程可以獲取到同一個Singleton對象,之后的線程調(diào)用getInstance()函數(shù),都會因為Instance不為空而直接返回,不會受到 synchronized 的性能影響。

java中volatile關(guān)鍵字作用
Java也支持volatile關(guān)鍵字,但它被用于其他不同的用途。當(dāng)volatile用于一個作用域時,Java保證如下:
(適用于Java所有版本)讀和寫一個volatile變量有全局的排序。也就是說每個線程訪問一個volatile作用域時會在繼續(xù)執(zhí)行之前讀取它的當(dāng)前值,而不是(可能)使用一個緩存的值。(但是并不保證經(jīng)常讀寫volatile作用域時讀和寫的相對順序,也就是說通常這并不是有用的線程構(gòu)建)。
(適用于Java5及其之后的版本)volatile的讀和寫建立了一個happens-before關(guān)系,類似于申請和釋放一個互斥鎖[8]。
使用volatile會比使用鎖更快,但是在一些情況下它不能工作。volatile使用范圍在Java5中得到了擴展,特別是雙重檢查鎖定現(xiàn)在能夠正確工作[9]。
上面有一個細節(jié),java 5版本之后volatile的讀與寫才建立了一個happens-before的關(guān)系,之前的版本會出現(xiàn)一個問題:Why is volatile used in this example of double checked locking,這個答案寫的很清楚了,線程 A 在完全構(gòu)造完 instance 對象之前就會給 instance 分配內(nèi)存,線程B在看到 instance 已經(jīng)分配了內(nèi)存不為空就回去使用它,所以這就造成了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].

eager initialization thread-safety (餓漢法,線程安全)

“餓漢法”就是在使用該變量之前就將該變量進行初始化,這當(dāng)然也就是線程安全的了,寫法也很簡單:

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;
}

代碼都很簡單,一個是直接進行初始化,另一個是使用靜態(tài)塊進行初始化,目的都是一個:在該類進行加載的時候就會初始化該對象,而不管是否需要該對象。這么寫的好處是編寫簡單,而且是線程安全的,但是這時候初始化instance顯然沒有達到lazy loading的效果。

static inner class thread-safety (靜態(tài)內(nèi)部類,線程安全)

由于在java中,靜態(tài)內(nèi)部類是在使用中初始化的,所以可以利用這個天生的延遲加載特性,去實現(xiàn)一個簡單,延遲加載,線程安全的單例模式:

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 的靜態(tài)內(nèi)部類,在該類中定義一個外部類 Singleton 的靜態(tài)對象,并且直接初始化,在外部類 Singleton 的 getInstance() 方法中直接返回該對象。由于靜態(tài)內(nèi)部類的使用是延遲加載機制,所以只有當(dāng)線程調(diào)用到 getInstance() 方法時才會去加載 SingletonHolder 類,加載這個類的時候又會去初始化 instance 變量,所以這個就實現(xiàn)了延遲加載機制,同時也只會初始化這一次,所以也是線程安全的,寫法也很簡單。

PS
  上面提到的所有實現(xiàn)方式都有兩個共同的缺點:
都需要額外的工作(Serializable、transient、readResolve())來實現(xiàn)序列化,否則每次反序列化一個序列化的對象實例時都會創(chuàng)建一個新的實例。
可能會有人使用反射強行調(diào)用我們的私有構(gòu)造器(如果要避免這種情況,可以修改構(gòu)造器,讓它在創(chuàng)建第二個實例的時候拋異常)。

enum (枚舉寫法)

JDK1.5 之后加入 enum 特性,可以使用 enum 來實現(xiàn)單例模式:

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

使用枚舉除了線程安全和防止反射強行調(diào)用構(gòu)造器之外,還提供了自動序列化機制,防止反序列化的時候創(chuàng)建新的對象。因此,Effective Java推薦盡可能地使用枚舉來實現(xiàn)單例。但是很不幸的是 android 中并不推薦使用 enum ,主要是因為在 java 中枚舉都是繼承自 java.lang.Enum 類,首次調(diào)用時,這個類會調(diào)用初始化方法來準(zhǔn)備每個枚舉變量。每個枚舉項都會被聲明成一個靜態(tài)變量,并被賦值。在實際使用時會有點問題,這是 google 的官方文檔介紹:
Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android
這篇博客也專門計算了 enum 的大?。汉鷦P-The price of ENUMs,所以枚舉寫法的缺點也就很明顯了。

登記式

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

//類似Spring里面的方法,將類名注冊,下次從里面直接獲取。  

public class Singleton {  
private static Map<String,Singleton> map = new HashMap<String,Singleton>();  
static{  
    Singleton single = new Singleton();  //構(gòu)造了一個簡單的實例
    map.put(single.getClass().getName(), single);  
}  
//保護的默認構(gòu)造子  
protected Singleton(){}  
//靜態(tài)工廠方法,返還此類惟一的實例  
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);  
}  
//一個示意性的商業(yè)方法  
public String about() {      
    return "Hello, I am RegSingleton.";      
}      
public static void main(String[] args) {  
    Singleton single3 = Singleton.getInstance(null);  
    System.out.println(single3.about());  
}  
}  

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

總結(jié)
  綜上所述,平時在 android 中使用** double-checked **或者 SingletonHolder 都是可以的,畢竟 android 早就不使用 JDK5 之前的版本了。由于 android 中的多進程機制,在不同進程中無法創(chuàng)建同一個 instance 變量,就像 Application 類會初始化兩次一樣,這點需要注意。

創(chuàng)建型模式 Rules of thumb

有些時候創(chuàng)建型模式是可以重疊使用的,有一些抽象工廠模式和原型模式都可以使用的場景,這個時候使用任一設(shè)計模式都是合理的;在其他情況下,他們各自作為彼此的補充:抽象工廠模式可能會使用一些原型類來克隆并且返回產(chǎn)品對象。
  抽象工廠模式,建造者模式和原型模式都能使用單例模式來實現(xiàn)他們自己;抽象工廠模式經(jīng)常也是通過工廠方法模式實現(xiàn)的,但是他們都能夠使用原型模式來實現(xiàn);
  通常情況下,設(shè)計模式剛開始會使用工廠方法模式(結(jié)構(gòu)清晰,更容易定制化,子類的數(shù)量爆炸),如果設(shè)計者發(fā)現(xiàn)需要更多的靈活性時,就會慢慢地發(fā)展為抽象工廠模式,原型模式或者建造者模式(結(jié)構(gòu)更加復(fù)雜,使用靈活);
  原型模式并不一定需要繼承,但是它確實需要一個初始化的操作,工廠方法模式一定需要繼承,但是不一定需要初始化操作;
  使用裝飾者模式或者組合模式的情況通常也可以使用原型模式來獲得益處;
  單例模式中,只要將構(gòu)造方法的訪問權(quán)限設(shè)置為 private 型,就可以實現(xiàn)單例。但是原型模式的 clone 方法直接無視構(gòu)造方法的權(quán)限來生成新的對象,所以,單例模式與原型模式是沖突的,在使用時要特別注意。

轉(zhuǎn)載ref: java/android 設(shè)計模式學(xué)習(xí)筆記(1)---單例模式

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容