應用最廣的模式——單例模式

《Android源碼設計模式解析與實戰》讀書筆記(二) 《Android源碼設計模式解析與實戰》PDF資料下載php

1、單例模式介紹

單例模式是應用最廣的模式之一。在應用這個模式時,單例對象的類必須保證只有一個實例存在。不少時候整個系統只須要擁有一個全局對象,這樣有利於咱們協調系統總體的行爲。設計模式

1.一、單例模式的定義

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

1.二、單例模式的使用場景

確保某個類有且只有一個對象的場景,避免產生多個對象消耗過多的資源,或者某種類型的對象只應該有且只有一個。bash

實現單例模式主要有以下幾個關鍵點微信

  • 構造函數不對外開放,通常爲Private;
  • 經過一個靜態方法或者枚舉返回單例類對象;
  • 確保單例類的對象有且只有一個,尤爲是在多線程環境下;
  • 確保單例類對象在反序列化時不會從新構建對象。

經過將單例類的構造函數私有化,使得不能經過new的形式手動構造單例類的對象。單例類會暴露一個公有靜態方法,須要調用這個靜態方法獲取到單例類的惟一對象,在獲取這個單例對象的和過程當中須要確保線程安全,即在多線程環境下構造單例類的對象也是有且只有一個,這也是單例模式實現中比較困難的地方。多線程

2、單例模式的簡單示例(餓漢式單例模式)

單例模式是設計模式中比較簡單的,只有一個單例類,沒有其餘的層次結構與抽象。併發

該模式須要確保該類只能生成一個對象,一般是該類須要消耗較多的資源或者沒有多個實例的狀況。ide

請看下面示例。 一個公司能夠有幾個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();

    //構造函數私有
    public CEO() {
    }

    //公有的靜態函數,對外暴露獲取單例對象的接口
    public static CEO getCeo() {
        return mCeo;
    }

    @Override
    public void work() {
        //管理VP
    }
}
複製代碼
//公司類
public class Company {
    private List<Staff> allStaffs=new ArrayList<Staff>();

    public void addStaff(Staff per) {
        allStaffs.add(per);
    }
    public void showAllStaffs(){
        for (Staff per :
                allStaffs) {
            Log.e("TAG","Obj:" + per.toString());
        }
    }
}
複製代碼
private void init() {
        Company cp=new Company();
        //CEO對象只能經過getCeo函數獲取
        Staff ceo1 = CEO.getCeo();
        Staff ceo2 = CEO.getCeo();
        cp.addStaff(ceo1);
        cp.addStaff(ceo2);

        //經過new建立VP對象
        Staff vp1 = new VP();
        Staff vp2 = new VP();

        //經過new建立Staff對象
        Staff mStaff1 = new Staff();
        Staff mStaff2 = new Staff();
        Staff mStaff3 = new Staff();

        cp.addStaff(vp1);
        cp.addStaff(vp2);
        cp.addStaff(mStaff1);
        cp.addStaff(mStaff2);
        cp.addStaff(mStaff3);

        cp.showAllStaffs();
    }
複製代碼

輸出結果以下:高併發

2019-01-07 23:13:26.925 5313-5313/? E/TAG: Obj:com.tengxin.mytest.CEO@4c4ca4b
2019-01-07 23:13:26.926 5313-5313/? E/TAG: Obj:com.tengxin.mytest.VP@e6f2c28
2019-01-07 23:13:26.926 5313-5313/? E/TAG: Obj:com.tengxin.mytest.VP@3ba2d41
2019-01-07 23:13:26.926 5313-5313/? E/TAG: Obj:com.tengxin.mytest.Staff@15542e6
2019-01-07 23:13:26.926 5313-5313/? E/TAG: Obj:com.tengxin.mytest.Staff@f93c027
2019-01-07 23:13:26.926 5313-5313/? E/TAG: Obj:com.tengxin.mytest.Staff@b9ee2d4
複製代碼

從代碼中能夠看到,CEO類不能經過new的形式構造對象,只能經過CEO.getCEO()函數來獲取。 這個實現的核心在於量CEO類的構造方法進行了私有化,使得外部程序不能經過構造函數來構造CEO對象,而CEO類經過一個靜態方法返回一個靜態對象。

3、單例模式的其餘實現方式

####3.一、懶漢式單例模式

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

懶漢式單例模式實現以下:

//懶漢式單例模式
public class Singleton {
    private static Singleton instance;

    private Singleton() {}

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

getInstance()方法中添加了synchronized關鍵字,使得在多線程狀況下保證單例對象惟一。

可是懶漢式單例模式也存在一個問題,就是每次調用getInstance方法都會進行同步,這樣會消耗沒必要要的資源。

懶漢式單例模式總結

  • 優勢:單例只有在使用時纔會被實例化,在必定程度上節約了資源;
  • 缺點:第一次加載時須要及時進行實例化,反應稍慢,最大的問題是每次調用getInstance都進行同步,形成沒必要要的同步開銷。

3.二、Double Check Lock(DCL)實現單例

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

代碼以下:

//DCL方式實現單例模式
public class DCLSingleton {
    private static DCLSingleton mSingleton = null;

    public DCLSingleton() {}

    public void doSomething(){
        Log.e("TAG", "do sth.");
    }

    public static DCLSingleton getInstance(){
        if (mSingleton == null) {
            synchronized (DCLSingleton.class) {
                if (mSingleton == null) {
                    mSingleton = new DCLSingleton();
                }
            }
        }
        return mSingleton;
    }
}
複製代碼

這種方式的亮點就在getInstance方法上,getInstance方法對mSingleton進行了兩次判空:

  1. 第一層判斷主要是爲了不沒必要要的同步;
  2. 第二層的判斷是爲了在null的狀況下建立實例。

DCL總結:

  • 優勢:資源利用率高,第一次執行getInstance時單例對象纔會被實例化,效率高。
  • 缺點:第一次加載時反應稍慢,也因爲Java內存模型的緣由偶爾會失敗。在高併發環境下也有必定的缺陷。

3.三、靜態內部類單例模式

DCL雖然在必定程度上解決了資源消耗、多餘的同步、線程安全等問題,可是,它仍是在某些狀況下出現失效的問題。這個問題被稱爲雙重檢查鎖定(DCL)失效。

建議使用以下代碼:

//靜態內部類單例模式
public class Singleton {
    private Singleton() {}

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

    /**
     * 靜態內部類
     */
    private static class SingletonHolder{
        private static final Singleton sInstance =new Singleton();
    }
}
複製代碼

當第一次加載Singleton類時並不會初始化sInstance,只有在第一次調用Singleton的getInstance方法時纔會致使sInstance被初始化。

這種方式不只可以確保線程安全,也可以保證單例對象的惟一性,同時也延遲了單例的實例化,這是推薦使用的單例模式實現方式。

3.四、枚舉單例

//枚舉單例
public enum SingletonEnum {
    INSTANCE;
    public void doSomething(){
        Log.e("TAG", "doSomething " );
    }
}
複製代碼

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

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

private Object readResolve() throws ObjectStreamException {
        return mSingleton;
    }
複製代碼

而對於枚舉,並不存在這個問題,由於即便反序列化也不會從新生成新的實例。

3.五、使用容器實現單例模式

下面是一種另類的實現:

//使用容器實現單例模式
public class SingletonManager {

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

    private SingletonManager() {}

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

    public static Object getService(String key) {
        return objMap.get(key);
    }
}
複製代碼

這種方式能夠管理多種類型的單例,而且在使用時能夠經過統一的接口進行獲取操做,下降了用戶的使用成本,也對用戶隱藏了具體實現,下降了耦合度。

不管什麼形式實現單例模式,其核心原理都是將構造函數私有化,而且經過靜態方法獲取一個惟一的實例,在獲取的過程當中要保證線程安全、防止反序列化致使從新生成實例對象等問題。

4、總結

單例模式是運用頻率很高的模式,在客戶端一般沒有高併發的狀況下,選擇哪一種實現方式並不會有太大的影響。出於效率考慮,推薦使用DCL形式單例模式、靜態內部類單例模式。

4.一、優勢

  1. 因爲單例模式在內存中只有一個實例,減小了內存開支,特別是一個對象須要頻繁地建立、銷燬時,並且建立或銷燬時性能又沒法優化,單例模式的優點就很是明顯;
  2. 因爲單例模式只生產一個實例,因此,減小了系統的性能開銷,當一個對象的產生須要比較多的資源時,則能夠經過在應用啓動時直接產生一個單例對象,而後用永久駐留內存的方式來解決;
  3. 單例模式能夠避免對資源的多重佔用,例如一個寫文件操做,因爲只有一個實例存在內存中,避免對同一個資源文件的同時寫操做;
  4. 單例模式能夠在系統設置全局的訪問點,優化和共享資源訪問,例如,能夠設計一個單例類,負責全部數據表的映射處理。

4.二、缺點

  1. 單例模式通常沒有接口,擴展很困難,若要擴展,除了修改代碼基本上沒有第二種途徑能夠實現;
  2. 單例對象若是持有Context,那麼很容易引起內存泄漏,此時須要注意傳遞給單例對象的Context最好是Application Context。

學海無涯苦做舟

個人微信公衆號
相關文章
相關標籤/搜索