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

HunterArley發表於2019-01-24

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

一、單例模式介紹

單例模式是應用最廣的模式之一。在應用這個模式時,單例物件的類必須保證只有一個例項存在。很多時候整個系統只需要擁有一個全域性物件,這樣有利於我們協調系統整體的行為。

1.1、單例模式的定義

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

1.2、單例模式的使用場景

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

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

  • 建構函式不對外開放,一般為Private;
  • 通過一個靜態方法或者列舉返回單例類物件;
  • 確保單例類的物件有且只有一個,尤其是在多執行緒環境下;
  • 確保單例類物件在反序列化時不會重新構建物件。

通過將單例類的建構函式私有化,使得不能通過new的形式手動構造單例類的物件。單例類會暴露一個公有靜態方法,需要呼叫這個靜態方法獲取到單例類的唯一物件,在獲取這個單例物件的和過程中需要確保執行緒安全,即在多執行緒環境下構造單例類的物件也是有且只有一個,這也是單例模式實現中比較困難的地方。

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

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

該模式需要確保該類只能生成一個物件,通常是該類需要消耗較多的資源或者沒有多個例項的情況。

請看下面示例。 一個公司可以有幾個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.1、懶漢式單例模式

懶漢式單例模式是宣告一個靜態物件,並且在使用者第一次呼叫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.2、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.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.4、列舉單例

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

列舉單例最大的優點就是寫法簡單,列舉在Java中與普通的類是一樣的,不僅能夠有欄位,還能夠有自己的方法。最重要的是預設列舉例項的建立是執行緒安全的,並且字啊任何情況下它都是一個單例。

通過序列化可以將一個單例的例項物件寫到磁碟,然後再讀回來,從而有效地獲得一個例項。即使建構函式是私有的,反序列化時依然可以通過特殊的途徑去建立類的一個新的例項,相當於呼叫該類的建構函式。反序列化操作提供了一個很特別的鉤子函式,類中具有一個私有的、被例項化的方法readResolve(),這個方法可以控制物件的反序列化。上述幾個示例中要杜絕單例物件在被反序列化時重新生成物件,必須加入如下方法:

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

而對於列舉,並不存在這個問題,因為即使反序列化也不會重新生成新的例項。

3.5、使用容器實現單例模式

下面是一種另類的實現:

//使用容器實現單例模式
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);
    }
}
複製程式碼

這種方式可以管理多種型別的單例,並且在使用時可以通過統一的介面進行獲取操作,降低了使用者的使用成本,也對使用者隱藏了具體實現,降低了耦合度。

無論什麼形式實現單例模式,其核心原理都是將建構函式私有化,並且通過靜態方法獲取一個唯一的例項,在獲取的過程中要保證執行緒安全、防止反序列化導致重新生成例項物件等問題。

四、總結

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

4.1、優點

  1. 由於單例模式在記憶體中只有一個例項,減少了記憶體開支,特別是一個物件需要頻繁地建立、銷燬時,而且建立或銷燬時效能又無法優化,單例模式的優勢就非常明顯;
  2. 由於單例模式只生產一個例項,所以,減少了系統的效能開銷,當一個物件的產生需要比較多的資源時,則可以通過在應用啟動時直接產生一個單例物件,然後用永久駐留記憶體的方式來解決;
  3. 單例模式可以避免對資源的多重佔用,例如一個寫檔案操作,由於只有一個例項存在記憶體中,避免對同一個資原始檔的同時寫操作;
  4. 單例模式可以在系統設定全域性的訪問點,優化和共享資源訪問,例如,可以設計一個單例類,負責所有資料表的對映處理。

4.2、缺點

  1. 單例模式一般沒有介面,擴充套件很困難,若要擴充套件,除了修改程式碼基本上沒有第二種途徑可以實現;
  2. 單例物件如果持有Context,那麼很容易引發記憶體洩漏,此時需要注意傳遞給單例物件的Context最好是Application Context。

學海無涯苦作舟

我的微信公眾號

相關文章