Android 設計模式の單例模式——應用最廣的模式

Chitty_Tina發表於2016-08-17

一、什麼是單例模式?

單例模式 就是 確保某一個類只有一個例項,而且自行例項化並向整個系統提供這個例項。在應用單例模式時,單例物件的類必須保證只有一個例項存在。許多時候整個系統只需要擁有一個全域性物件,這樣有利於我們協調系統整體的行為。

二、單例模式的使用場景

不能自由構造物件的情況,確保某個類有且只有一個物件的場景,避免產生多個物件消耗過多的資源,或者某種型別的物件只應該有且只有一個。例如,建立一個物件需要消耗的資源過多,如要訪問 IO 和資料庫等資源,這是就要考慮使用單例模式。

三、實現單例模式的關鍵點

1. 建構函式不對外開放,一般為 Private;
通過將單例類的建構函式私有化,使得客戶端程式碼不能通過 new 的形式手動構造單例類的物件。
2. 通過一個靜態方法或者列舉返回單例類物件;
單例類會暴露一個公有靜態方法,客戶端需要呼叫這個靜態方法獲取到單例類的唯一物件。
3. 確保單例類物件有且只有一個,尤其是在多執行緒環境下;
在獲取這個單例物件的過程中需要確保執行緒安全,即在多執行緒環境下構造單例類的物件也是有且只有一個。(實現較困難)
4.確保單例類物件在反序列化時不會重新構建物件。

四、單例模式的幾種實現方式

1. 餓漢模式

public class Singleton{
    private static final Singleton mInstance = new Singleton();
    // 建構函式私有
    private Singleton(){
    }
    // 公有的靜態函式,對外暴露獲取單例物件的介面
    public static Singleton getInstance(){
        return mInstance;
    }
}

餓漢模式是在宣告一個靜態物件時就對其初始化。

  1. 缺點反序列化時會出現重新建立物件的情況。

2. 懶漢模式

public class Singleton{
    private static Singleton mInstance;
    private Singlrton(){
    }
    public static synchronized Singleton getInstance(){
        if(mInstance == null){
            mInstance = new Singleton();
        }
        return mInstance;
    }
}

懶漢模式是宣告一個靜態物件,並且在使用者第一次呼叫 getInstance 時進行初始化。

  1. 優點:單例只有在使用時才會被例項化,在一定程度上節約了資源;
  2. 缺點:第一次載入時需要及時進行例項化,反應慢;反序列化時會出現重新建立物件的情況;
  3. 最大的問題:getInstance()方法中新增了 synchronized 關鍵字,於是乎,每次呼叫getInstance()都進行同步,造成不必要的同步開銷;
  4. 結論:懶漢模式一般不建議使用

3.Double CheckLock(DCL)實現單例(推薦使用)

public class Singleton(){
    private volatile static Singleton mInstance = null;
    private Singleton(){
    }
    public void doSomething(){
        System.out.println("Do sth.");
    }
    public static Singleton getInstance(){
        if(mInstance == null){
            synchronized(Singleton.class){
                if(mInstance == null){
                    mInstance = new SIngleton();
                }
            }
        }
        return mInstance;
    }
}

其中,加上 volatile 關鍵字後,可以解決 DCL 失效問題,但或多或少會影響到效能,但考慮到程式的正確性,犧牲這點效能還是值得的。

  1. 優點:既能夠在需要時才初始化單例,並能在絕大多數場景下保證單例物件的唯一性,又能夠保證執行緒安全,且單例物件初始化後呼叫 getInstance 不進行同步鎖。資源利用率高,第一次執行 getInstance 時單例物件才會被例項化,效率高;
  2. 缺點:第一次載入時反應稍慢,也由於 Java 記憶體模型的原因偶爾會失敗。在高併發環境下也有一定的缺陷,雖然發生概率很小;反序列化時會出現重新建立物件的情況;
  3. 結論:除了 併發場景比較複雜 或者 低於 JDK 6 版本 的情況,DCL模式是使用最多的單例實現方式。

4.靜態內部類單例模式(推薦使用)

public class Singleton(){
    private Singleton(){
    }
    public static Singleton getInstance(){
        return SingletonHolder.mInstance;
    }
    /**
     * 靜態內部類
     */
     private static class SingletonHolder(){
         private static final Singleton mInstance = new Singleton();
     }
}

雖然 DCL 在一定程度上解決了資源消耗、多餘同步、執行緒安全等問題,但它在某些情況下會出現 雙重檢查鎖定(DCL)失效 問題,所以,建議用 靜態內部類單例模式 代替。

  1. 優點:能夠保證執行緒安全,能夠保證單例物件的唯一性,延遲了單例的例項化;
  2. 缺點反序列化時會出現重新建立物件的情況
  3. 結論:推薦使用該方式實現單例模式。

5.列舉單例

public enum SingletonEnum{
    INSTANCE;
    public void doSomething(){
        System.out.println("Do sth.");
    }
}
  1. 優點:寫法簡單;預設列舉例項的建立是執行緒安全的;任何情況下它都是一個單例,即使是反序列化時

6.使用容器實現單例模式

public class SingletonManager{ 
    private static Map<String,Object> objMap = new HashMap<String,Object>();
    private Singleton(){
    }
    public static void registerService(String key,Object mInstance){ 
        if(!objMap.containKey(key)){ 
            objMap.put(key,mInstance);
        }
    }
    public static ObjectgetService(String key){ 
        return objMap.get(key);
    }
}
  1. 優點:降低使用者的使用成本,對使用者隱藏了具體實現,降低了耦合度。

五、總結

  1. 核心原理:將建構函式私有化,且通過靜態方法獲取一個唯一的例項,在此獲取過程中必須保證執行緒安全、防止反序列化導致重新生成例項物件等問題。

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

  3. 缺點
    (1)、單例模式一般沒有介面,擴充套件很困難,除了修改程式碼;
    (2)、單例物件如果持有 Context ,那麼很容易引發記憶體洩漏,此時需要注意傳遞給單例物件的 Context 最好是 Application Context 。

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

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

即在 readResolve() 方法中將 mInstance 物件返回,而不是預設的重新生成一個新的物件。

相關文章