創(chuàng)建型設(shè)計模式——單例模式

面向?qū)ο蟮牧笤瓌t

  • 單一職責原則 Single Responsibility Principle
    一個類中應(yīng)該是一組相關(guān)性很高的函數(shù)、數(shù)據(jù)的封裝。兩個完全不一樣的功能就不應(yīng)該放在一個類中。

  • 開閉原則 Open Close Principle
    軟件中的對象(類、模塊、函數(shù)等)應(yīng)該對于擴展是開放的,但是對于修改是封閉的。
    在軟件的生命周期內(nèi),因為變化、升級、維護等原因需要對軟件原有的代碼進行修改時,可能會將錯誤引入原本已經(jīng)經(jīng)過測試的舊代碼中,破壞原有系統(tǒng)。因此當軟件需要變化時,我們應(yīng)該盡量通過擴展的方式來實現(xiàn)變化,而不是通過修改已有的代碼來實現(xiàn)。盡量,說明OCP原則并不是說絕對不能修改原始類的,當原始類不好的時候應(yīng)該盡早的進行重構(gòu)。

  • 里氏替換原則 Liskov Substitution Principle
    所有引用基類的地方,必須能透明地使用其子類的對象。里氏替換原則就是依賴于繼承和多態(tài)這兩大特性。
    開閉原則和里氏替換原則往往是相互依賴的,通過里氏替換來達到對擴展開放,對修改 封閉的效果。

  • 依賴倒置原則 Dependence Inversion Principle
    一種特定的解耦形式,使得高層次的模塊不依賴底層次的模塊。具體而言即:1)高層模塊不應(yīng)該依賴低層模塊,兩者都應(yīng)該依賴其抽象;2)抽象不應(yīng)該依賴細節(jié);3)細節(jié)應(yīng)該依賴抽象。
    在Java語言中,一般抽象是指接口或者抽象類,兩者都無法被實例化。細節(jié)是指實現(xiàn)類,實現(xiàn)接口或繼承抽象類而產(chǎn)生的類就是細節(jié)。
    模塊間的依賴通過抽象發(fā)生,實現(xiàn)類之間不發(fā)生直接的依賴關(guān)系,其依賴關(guān)系是通過接口或抽象類產(chǎn)生的。

  • 接口隔離原則 Interface Segregation Principle
    客戶端不應(yīng)該依賴它不需要的接口。目的是系統(tǒng)解耦,從而更容易重構(gòu)、更改和重新部署。
    上述是面向?qū)ο缶幊痰?個基本原則。當這些原則被一起應(yīng)用時,它們使得一個軟件系統(tǒng)更清晰、簡單,最大程度地擁抱變化。

  • 迪米特原則 最少知識原則 Least Knowledge Principle
    一個類應(yīng)該對自己需要耦合或調(diào)用的類知道得最少,類的內(nèi)部如何實現(xiàn)與調(diào)用者或者依賴者沒關(guān)系,調(diào)用者或者依賴者只需要知道它需要的方法即可,其他的可一概不管。


單例模式是應(yīng)用最廣的模式之一。

定義

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

以上可知單例類的特點:

  • 只能有一個實例
  • 必須自己創(chuàng)建自己的唯一實例
  • 必須給其他對象(整個系統(tǒng))提供這一實例

使用場景

當這個類的對象在多個地方創(chuàng)建的時候,使得內(nèi)部的方法多次調(diào)用,但是我們希望只要一個對象操作這個方法,或者不希望多個地方同時調(diào)用這個方法,我們需要保持這個方法的單一性質(zhì),我們就用單例模式。

比如:訪問IO和數(shù)據(jù)庫資源、單一彈框等。

實現(xiàn)單例模式的關(guān)鍵點

  • 構(gòu)造函數(shù)不對外開放,一般為Private
  • 通過一個靜態(tài)方法或者枚舉返回單例類對象
  • 確保單例類的對象有且僅有一個,尤其是在多線程環(huán)境下
  • 確保單例類對象在反序列化時不會重新構(gòu)建對象
單例模式UML類圖

單例模式的實現(xiàn)方法

1.懶漢模式
//懶漢式單例類.在第一次調(diào)用的時候?qū)嵗约?public class LHS_Singleton {

    private static LHS_Singleton instance;

    private LHS_Singleton() {
    }

    //靜態(tài)工廠方法
    public static synchronized LHS_Singleton getInstance() {
        if (instance == null) {
            instance = new LHS_Singleton();
        }
        return instance;
    }
}

getInstance()方法中用到了synchronized同步鎖,保證了線程安全。
懶漢式是典型的時間換空間

  • 優(yōu)點:單例只有在使用的時候才會被實例化,一定程度上節(jié)約了資源。
  • 缺點:第一次加載的時候需要及時進行實例化,最大問題在于每次調(diào)用都要進行同步,會造成不必要的同步開銷,降低整個訪問速度,且每次都需要進行判斷是否需要創(chuàng)建實例。
2.DCL雙重檢查加鎖模式
public class DCL_Singleton {
    private volatile static DCL_Singleton singleton = null;

    private DCL_Singleton() {
    }

    public static DCL_Singleton getSingleton() {
        //先檢查實例是否存在,如果不存在才進入下面的同步塊
        if (singleton == null) {
            //同步塊,線程安全的創(chuàng)建實例
            synchronized (DCL_Singleton.class) {
                //再次檢查實例是否存在,如果不存在才真正的創(chuàng)建實例
                if (singleton == null) {
                    singleton = new DCL_Singleton();
                }
            }
        }
        return singleton;
    }
}

與懶漢式不同在于,在getInstance()方法中不用再進行同步鎖。第一個判斷singleton == null主要是為了避免不必要的同步,第二次判斷則是為了singleton = null的時候在線程安全的情況下創(chuàng)建實例。

  • 優(yōu)點:資源利用率高,既能夠在需要的時候才初始化單例,又能保證線程安全,且單例對象初始化后調(diào)用getInstance()不再進行同步鎖。
  • 缺點:第一次加載反應(yīng)稍慢,由于Java內(nèi)存模型原因偶爾會失敗,高并發(fā)情況下有一定缺陷。
  • 注:由于volatile關(guān)鍵字可能會屏蔽掉虛擬機中一些必要的代碼優(yōu)化,所以運行效率并不是很高。因此雖然可以使用“雙重檢查加鎖”機制來實現(xiàn)線程安全的單例,但并不建議大量采用,可以根據(jù)情況來選用。
3.靜態(tài)內(nèi)部類模式
public class JTNBL_Singleton {
    private JTNBL_Singleton() {
    }

    public static JTNBL_Singleton getInstanse() {
        return SingletonHolder.instance;
    }

    /**
     *    類級的內(nèi)部類,也就是靜態(tài)的成員式內(nèi)部類,該內(nèi)部類的實例與外部類的實例
     *    沒有綁定關(guān)系,而且只有被調(diào)用到時才會裝載,從而實現(xiàn)了延遲加載。
     */
    private static class SingletonHolder {
        /**
         * 靜態(tài)初始化器,由JVM來保證線程安全
         */
        private static final JTNBL_Singleton instance = new JTNBL_Singleton();
    }
}

getInstance()方法第一次被調(diào)用的時候,它第一次讀取SingletonHolder.instance,會使SingletonHolder類得到初始化;而這個類在裝載并被初始化的時候,會初始化它的靜態(tài)域,從而創(chuàng)建JTNBL_Singleton的實例,由于是靜態(tài)的域,因此只會在虛擬機加載類的時候初始化一次,并由虛擬機來保證它的線程安全性。

優(yōu)點:這種方式不僅能夠確保線程安全,也能夠保證單例對象的唯一性,同時也延遲了單例的實例化。

在上述的幾種實現(xiàn)單例模式的情況中,有一個情況下他們會出現(xiàn)重新創(chuàng)建對象的情況,那就是反序列化。如果要杜絕單例對象在被反序列化時重新生成對象,那么必須加入下面的方法:

private Object readResolve() throws ObjectStreamException {
        return instance;
    }
4.枚舉單例

對于枚舉,則不存在反序列化問題,因為即使反序列化枚舉單例也不會重新生成新的實例。

public enum MJ_Singleton {
    /**
     * 定義一個枚舉的元素,它就代表了MJ_Singleton的一個實例。
     */
    INSTANCE;

    /**
     * 單例可以有自己的操作
     */
    public void doSomething() {
        System.out.println("Hello World");
    }
}

枚舉在JAVA中與普通的類一樣,不僅能有字段,還能有自己的方法,最重要的是默認枚舉實例的創(chuàng)建是線程安全的,并且在任何情況下它都是一個單例。

按照《高效Java 第二版》中的說法:單元素的枚舉類型已經(jīng)成為實現(xiàn)Singleton的最佳方法。用枚舉來實現(xiàn)單例非常簡單,只需要編寫一個包含單個元素的枚舉類型即可。

5.使用容器實現(xiàn)單例模式
public class RQ_Singleton {
    private static Map<String, Object> objectMap = new HashMap<>();

    private RQ_Singleton() {
    }

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

    public static Object getService(String key) {
        return objectMap.get(key);
    }
}

在程序初始,將多種單例類型注入到一個統(tǒng)一的單例管理類中,通過key來獲取對應(yīng)類型的對象。

這種方式使得我們可以管理多種類型的單例,并且在使用時可以通過統(tǒng)一的接口進行獲取操作,降低了耦合度,對用戶隱藏了具體實現(xiàn)。

6.餓漢模式
public class EHS_Singleton {
    // 在這個類被加載的時候,靜態(tài)變量single會被初始化,
    // 此時類的私有構(gòu)造子會被調(diào)用。這時單例類的唯一實例就被構(gòu)造出來了。
    private static EHS_Singleton instance = new EHS_Singleton();

    private EHS_Singleton() {
    }

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

餓漢式其實是一種比較形象的稱謂。既然餓,那么在創(chuàng)建對象實例的時候就比較著急,餓了嘛,于是在裝載類的時候就創(chuàng)建對象實例。

餓漢式是典型的空間換取時間,當類裝載時就會創(chuàng)建類的實例,不管你用不用,先創(chuàng)建出來,然后每次調(diào)用的時候,就不需要再判斷,節(jié)省了運行時間。

單例模式的優(yōu)點

  • 在內(nèi)存中只有一個實例,減少了內(nèi)存開支,特別是對于一個對象需要頻繁地創(chuàng)建、銷毀時,而且創(chuàng)建或銷毀時性能又無法優(yōu)化,單例模式的優(yōu)勢便非常明顯。
  • 當一個對象的產(chǎn)生需要比較多的資源時,如讀取配置、產(chǎn)生其他依賴對象時,則可以通過在應(yīng)用啟動時直接產(chǎn)生一個單例對象,然后永久駐留內(nèi)存的方式來解決。
  • 單例模式可以避免對資源的多重占用,例如寫文件操作,避免了對同一個資源文件的同時寫操作。
  • 單例模式可以在系統(tǒng)設(shè)置全局的訪問點,優(yōu)化和共享資源訪問,例如可以設(shè)計一個單例類,負責所有數(shù)據(jù)表的映射處理。

單例模式的缺點

  • 單例模式一般沒有接口,擴展很困難,若要擴展,除了修改代碼基本上沒有第二種途徑可實現(xiàn)
  • 單例對象如果持有Context,那么很容易引發(fā)內(nèi)存泄漏,此時需要注意傳遞給單例對象的Context最好是Application Context



引用:
Android 源碼設(shè)計模式解析與實戰(zhàn)》
Java設(shè)計模式之單例模式及在Android中的重要使用

最后編輯于
?著作權(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)容