[Android]單例模式

概述

單例模式是應用最廣的模式之一,在應用這個模式時,單例對象的類必須保證只有一個實例存在。許多時候整個系統只須要擁有一個全局對象,這樣有利於咱們協調系統總體的行爲。如在一個應用中,應該只有一個ImageLoader實例,這個ImageLoader中又含有線程池、緩存系統、網絡請求等,很消耗資源。所以不該該讓它構造多個實例。這樣不能自由構造對象的狀況,就是單例模式的使用場景。數據庫

定義

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

使用場景

確保某個類有且只要一個對象的場景,避免產生多個對象消耗過多的資源,或者某種類型的對象只應該有且只有一個。例如,建立一個對象須要消耗的資源過多,如要訪問IO和數據庫等資源,這時就要考慮使用單例模式。設計模式

UML類圖

單例模式的UML類圖以下:緩存

角色介紹:安全

  • Client:高層客戶端網絡

  • Singleton:單例類多線程

實現單例模式主要有如下幾個關鍵點:併發

  • 構造函數不對外開放,通常爲private;ide

  • 經過一個靜態方法或者枚舉返回單例類對象;函數

  • 確保單例類的對象有且只有一個,尤爲是在多線程環境下;

  • 確保單例類對象在反序列化時不會從新構建對象;

單例模式中實現比較困難的是在多線程環境下構造單例類的對象有且只有一個。

簡單示例

單例模式在設計模式中是結構比較簡單的,只有一個單例類,沒有其餘層次結構和抽象。該模式須要確保該類只能生成一個對象,一般是該類須要消耗較多的資源或者沒有對個實例的狀況。例如一個公司只有一個CEO、一個應用只有一個Application對象等。

下面以公司裏的CEO爲例來簡單演示一下,一個公司能夠有多個VP、無數個員工,但只有一個CEO,代碼以下:

/**
 * 
 * 普通員工
 *
 */
public class Staff {
    
    public void work() {
        //幹活
    }


}

//副總裁
public class VP extends Staff {

    @Override
    public void work() {
        // 管理下面的經理
        
    }
}

//CEO,餓漢式單例
public class CEO extends Staff {
    
    private static final CEO mCEO = new CEO();
    
    private CEO() {
        
    }

    //公有的靜態函數,對外暴露獲取單例對象的接口
    public static CEO getCeo() {
        return mCEO;
    }
    @Override
    public void work() {
        // 管理VP
    }
}

//公司類
public class Company {

    private List<Staff> mStaffs = new ArrayList<Staff>();
    
    public void addStaff(Staff staff) {
        mStaffs.add(staff);
    }
    
    public void showStaffs() {
        for(Staff staff : mStaffs) {
            System.out.println("Obj: " + staff.toString());
        }
    }
}

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        
        Company company = new Company();
        
        //CEO對象只能經過getCeo獲取
        Staff ceo1 = CEO.getCeo();
        Staff ceo2 = CEO.getCeo();
        company.addStaff(ceo1);
        company.addStaff(ceo2);
        
        Staff vp1 = new VP();
        Staff vp2 = new VP();
        company.addStaff(vp1);
        company.addStaff(vp2);
        
        Staff staff1 = new Staff();
        Staff staff2 = new Staff();
        
        company.addStaff(staff1);
        company.addStaff(staff2);

        company.showStaffs();
    }

運行輸出結果以下:

Obj: com.liuguoquan.design.single.CEO@15db9742
Obj: com.liuguoquan.design.single.CEO@15db9742
Obj: com.liuguoquan.design.single.VP@6d06d69c
Obj: com.liuguoquan.design.single.VP@7852e922
Obj: com.liuguoquan.design.single.Staff@4e25154f
Obj: com.liuguoquan.design.single.Staff@70dea4e

從上面代碼能夠看出,CEO類不能經過new的形式構造函數,只能經過CEO.getCeo()方法來獲取,而這個CEO對象是靜態對象,而且在聲明的時候就已經初始化,這就保證類CEO對象的惟一性。

從輸出結果中能夠看出,CEO兩次輸出的CEO對象的地址都同樣,說明是同一個CEO對象;而VP、Staff等類型的對象都是不一樣的。

實現方式

餓漢式

餓漢式模式是在聲明靜態對象時就已經初始化,這種方式簡單粗暴,若是單例對象初始化很是快,並且佔用內存小的時候這種方式是比較適合的,能夠直接在應用啓動時加載初始化。實現以下:

public class Singleton {

    private static Singleton instance = new Singleton();

    private Singleton() {

    }

    public static Singleton getInstance() {
        return instance;
    }
}

懶漢式

懶漢模式是聲明一個靜態對象,而且在用戶第一次調用getInstance時進行初始化。

public class Singleton {

    private static Singleton instance = null;

    private Singleton() {

    }

    public static Singleton getInstance() {

        synchronized(Singleton.class) {
            if (instance == null) {
                instance = new Singleton();
            }
        return instance;
    }
}

getInstance方法中添加了Synchronized關鍵字,也就是同步類synchronized關鍵字包含的代碼塊,這就是上面所說的在多線程中保證單例對象惟一性的手段。可是仍存在一個問題,即便instance已經初始化,每次調用getInstance方法都會進行同步,這樣會消耗沒必要要的資源,這也是懶漢式存在的最大問題

懶漢單例模式的優勢是隻有在使用時纔會被實例化,在必定程度上節約了資源,缺點是第一次加載時須要及時進行實例化,反應稍慢,最大問題是每次調用geInstance都進行同步,形成沒必要要的同步開銷,這樣模式通常不建議使用。

Double CheckLock(雙重校驗鎖)

DCL方式的優勢是既可以在須要時才初始化單例,又可以保證線程的安全,且單例對象初始化後調用getInstance不獲取同步鎖。

public class Singleton {

    //private static volatile Singleton instance = null;
    private static Singleton instance = null;

    private Singleton() {

    }

    public static Singleton getInstance() {
        //若是已經初始化,不須要每次獲取同步鎖
        if(instance == null) {
            synchronized(Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
        }
        return instance;
    }
}

能夠看到getInstance方法對instance進行了兩次判空:第一層判斷主要是爲了不沒必要要的同步,第二層判斷主要則是爲了在null的狀況下建立實例。下面,咱們來分析一下:

假設線程A執行到instance=new Singleton()語句,這裏看起來是一句代碼,但實際上它並非一個原子操做,這局代碼最終會被編譯成多條彙編指令,它大體作了3件事情:

  1. 給Singleton的實例分配內存

  2. 調用Singleton()的 構造函數,初始化字段成員

  3. 將instance對象執行分配的內存空間(此時instance就不是null了)

可是,因爲Java編譯器運行處理器亂序執行,以及jdk1.5以前Java內存模型中Cache、寄存器到主內存會寫順序的規定,上面的第二和第三的順序是沒法保證的。也就是說,執行順序多是1-2-3也多是1-3-2.若是是後者,而且在3執行完畢、2未執行以前,被切換到線程B上,這時候instance由於已經在線程A內執行3了,instance已是非null,全部線程B直接取走instance,再使用時就會出錯,這就是DCL失效問題,並且這種難以跟蹤難以重現的問題極可能會隱藏好久。

在jdk1.5以後,官方已經注意到這種問題,調整了JMM、具體化了volatile關鍵字,所以,若是是1.5或以後的版本,只須要將instance的定義改爲private static volatile Singleton instance = null;就能夠保證instance對象每次都是從主內存中讀取,就可使用DCL的寫法來完成單例模式。固然,volatile多少會影響到性能,但考慮到程序的正確性,犧牲這點性能仍是值得的。

DCL的優勢:資源利用率高,第一次執行getInstance時單例對象纔會被實例化,效率高。

缺點:第一次加載稍慢,也因爲Java內存模型的緣由偶爾會失敗。在高併發的環境下也有必定的缺陷,雖然機率發生很小。

DCL模式是使用最多的單例實現模式,它可以在須要時才實例化單例對象,而且可以在絕大多數場景下保證單例對象的惟一性,除非你的代碼在併發場景比較複雜或者低於jdk1.6版本下使用,不然這種方式通常可以知足需求。

靜態內部類單例模式

在《Java併發編程實戰》中談到不同意使用DCL的優化方式,而建議使用以下代碼替代:

public class Singleton {

    private Singleton() {}

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }

    //靜態內部類
    private static class SingletonHolder {
        private static final Singleton instance = new Singleton();
    }
}

當第一次加載Singleton類時並不會初始化instance,只有第一次調用Singleton的getInstance方法時纔會致使instance被初始化。所以,第一次調用getInstance方法會致使虛擬機加載SingletonHolder類,這種方式不只可以確保線程安全,也可以保證單例對象的惟一性,同時也延遲了單例的實例化,因此這是推薦使用的單例模式實現方式。

枚舉單例

public enum Singleton {

    //定義一個枚舉的元素,它就是Singleton的一個實例
    INSTANCE;

    public void doSomething() {

    }
}

//使用
public static void main(String[] args){
   Singleton singleton = Singleton.instance;
   singleton.doSomething();
}

寫法簡單是枚舉單例最大的優勢,枚舉在Java中與普通的類是同樣的,不只可以有字段,還可以有本身的方法。最重要的是默認枚舉實例的建立時線程安全的,而且在任何狀況下它都是一個單例。

爲何這麼說呢?在上述的幾種單例模式實現中,在一個狀況下它們會出現從新建立對象的狀況,那就是反序列化。

經過序列化能夠將一個單例的實例對象寫到磁盤,而後再讀回來,從而有效地得到一個實例。即便構造函數時私有的,反序列化時依然能夠經過特殊的途徑去建立類的一個新的實例,至關於調用該類的構造函數。反序列化操做提供一個很特別的鉤子函數,類中具備一個私有的、被實例化的方法readResolve(),這個方法可讓開發人員控制對象的反序列化。例如,上述幾個實例中若是要杜絕單例對象在被反序列化時從新生成對象,那麼必須加入以下方法:

private Object readResolve() throws ObjectStreamException {
    return instance;
}

也就是在readResolve方法中將instance對象返回,而不是默認的從新生成一個新的對象。而對於枚舉並不存在這樣的問題,由於即便反序列化它也不會從新生成新的實例。

容器管理單例

public class SingletonManager {

    private static Map<String,Object> objMap = new HashMap<String,Object>();

    public static void registerService(String key,Object instance) {
        if (!objMap.containsKey(key)) {
            objMap.put(key);
        }
    }

    public static getInstance(String key) {
        return objMap.get(key);
    }
}

在程序的初始,將多種單例類注入到一個統一的管理類中,在使用根據key獲取對應類型的對象,這種方式使得咱們能夠管理不少類型的單例,而且在使用它們的時候能夠經過統一的接口進行獲取操做操做,下降用戶的使用成本,也對用戶隱藏了具體實現,下降了耦合度。

總結

單例模式是運用頻率很高的模式,可是,因爲在客戶端一般沒有高併發的狀況,所以,選擇哪一種實現方式並不會有太大的影響。即使如此,出於效率考慮,推薦使用雙重校驗鎖和靜態內部類單例模式。

優勢

  • 因爲單例模式在內存中只有一個實例,減小了內存開支,特別是一個對象須要頻繁建立、銷燬時,並且建立或者銷燬時性能又沒法優化,單例模式的優點就很是明顯。

  • 因爲單例模式只生成一個實例,因此,減小了系統的性能開銷,當一個對象的產生須要比較多的資源時,如讀取配置、產生依賴對象時,則能夠經過在應用啓動時直接產生一個單例對象,而後用永駐內存的方式解決。

  • 單例模式能夠避免對資源的多重佔用,例如一個寫文件操做,因爲只有一個實例存在內存中,避免對同一個資源文件的同時寫操做。

  • 單例模式能夠在系統設置全局訪問點,優化和共享資源訪問,例如,能夠設計一個單例類,負責全部數據表的映射處理。

缺點:

  • 單例模式通常沒有接口,擴展很困難,若要擴展,除了修改代碼基本沒有第二種途徑能夠實現。

  • 在Android中,單例對象若是持有Context,那麼很容易引起內存泄露,此時須要注意傳給單例對象的Context最好是Application Context。

相關文章
相關標籤/搜索