[設計模式]單例設計模式的6種實現方式(超全面喲)

無論什麼開發中,設計模式都起着關鍵的作用,其中比較常用的當屬單例了~那麼什麼是單例設計模式呢?

1. 什麼是單例設計模式(SINGLETON)

單例模式指的是在應用整個生命週期內只能存在一個實例。單例模式是一種被廣泛使用的設計模式。他有很多好處,能夠避免實例對象的重複創建,減少創建實例的系統開銷,節省內存。

有一個比喻:俺有6個漂亮的老婆,她們的老公都是我,我就是我們家裏的老公Sigleton,她們只要說道“老公”,都是指的同一個人,那就是我(剛纔做了個夢啦,哪有這麼好的事)。

在java語言中,單例帶來了兩大好處:

1)對於頻繁使用的對象,可以省略創建對象所花費的時間,這對於那些重量級的對象而言,是非常可觀的一筆系統開銷。

2)由於new操作的次數減少,因而對系統內存的使用頻率也會降低,這將減輕GC壓力,縮短GC停頓時間。

所以對於系統的關鍵組件和被頻繁操作的對象,使用單例模式便可以有效地改善系統性能。

那我們使用靜態類創建唯一實例不可以嗎?那這裏就需要說一下 單例 和 靜態類 的區別了。

2. 單例模式和靜態類的區別

首先理解一下什麼是靜態類,靜態類就是一個類裏面都是靜態方法和靜態屬性,構造器被private修飾,因此不能被實例化。

知道了什麼是靜態類後,來說一下他們兩者之間的區別:

1)首先單例模式會提供給你一個全局唯一的對象,靜態類只是提供給你很多靜態方法,這些方法不用創建對象,通過類就可以直接調用;

2)單例模式的靈活性更高,方法可以被override,因爲靜態類都是靜態方法,所以不能被override;

3)如果是一個非常重的對象,單例模式可以懶加載,靜態類就無法做到;

那麼時候時候應該用靜態類,什麼時候應該用單例模式呢?首先如果你只是想使用一些工具方法,那麼最好用靜態類,靜態類比單例類更快,因爲靜態的綁定是在編譯期進行的。如果你要維護狀態信息,或者訪問資源時,應該選用單例模式。還可以這樣說,當你需要面向對象的能力時(比如繼承、多態)時,選用單例類,當你僅僅是提供一些方法時選用靜態類。

那麼我們該如何去實現單例設計模式呢?

3.單例設計模式的實現方式

(1)餓漢式(我很餓,直接把吃的交上來,我一直在)

public class Singleton {   
    private Singleton() {}                       //1.私有構造函數,不讓別的類創建本類對象
    private static Singleton = new Singleton();  //2.創建本類對象
    public static getSignleton(){                //3.對外提供公共的訪問方法,返回本類對象
        return singleton;
    }
}

/*此處還可以使用final來實現
    public class Singleton {   
        private Singleton() {}                             
        private final static Singleton = new Singleton();  
   
    }
*/

餓漢式是在第一次引用該類的時候就創建對象實例,而不管實際是否需要創建,假如單例類裏面有getSignleton()以外的其它靜態方法,跟單例沒啥關係的方法,如果使用了Singleton.Xxx()這種調用,就會自動創建Singleton這個類實例(雖然private構造已經防止了你人爲去new這個單例),這是開發人員不願看到的。

好處是編寫簡單,但是佔用資源,無法做到延遲創建對象,因此這種方式適合佔用資源少,在初始化的時候就會被用到的類。

我們很多時候都希望對象可以儘可能地延遲加載,從而減小負載,所以就需要下面的懶漢法:

(2)懶漢式(我很懶,啥時候需要我,我再出來)

public class Singleton {
    private Singleton(){}                                 //1.私有構造方法
    private static Singleton singleton = null;            //2.創建本類對象的屬性並賦值爲null            
    public static Singleton getSingleton() {              //3.對外提供公共的訪問方法
        if(singleton == null)                             //在類中判斷本類對象是否爲空
            singleton = new Singleton();                  //如果爲空則創建對象並賦值
        return singleton;                                 //返回本類對象
    }
}

懶漢模式就是單例的延遲加載,也叫懶加載。在程序需要用到的時候再創建實例,這樣保證了內存不會被浪費。

這次改良主要是從JVM加載類的原理上進行改良的,上面的代碼在初始化類的時候給singleton賦予null,確保系統啓動的時候沒有額外的負載,其次,在getSingleton()方法中加入判斷單例是否存在,不存在才new。

這樣寫如果是單線程下運行是沒有問題的,但如果是在多線程中就可能會出現兩個或多個對象,試想一下如果恰好有兩個線程同時進入了getSingleton()得if語句裏面,這時候就會實例化兩次Singleton,因爲首次執行getSingleton()時singleton是null,所以這種情況是可能發生的。

所以針對以上兩種實現方式,區別就出來了

1)如果都是單線程:

餓漢式是以空間換時間

懶漢式是以時間換空間

2)如果都是多線程:

餓漢式是沒有安全隱患的

懶漢式可能會創建多個對象

那我們如何保證懶漢式的線程安全呢?

(3)線程安全下的懶漢式:

public class Singleton {
    private Singleton(){}
    private static volatile Singleton singleton = null;
    public static Singleton getSingleton(){
        synchronized (Singleton.class){
            if(singleton == null){
                singleton = new Singleton();
            }
        }
        return singleton;
    }    
}

爲了在多線程中不讓兩個線程同時執行getSingleton()方法,可以爲此方法添加一同步鎖將對singleton的null判斷以及new的部分使用synchronized進行加鎖(同步方法或同步代碼塊都可以),同時,對singleton對象使用volatile關鍵字進行限制,保證其對所有線程的可見性(這裏volitile關鍵字的作用是,當多線程實例化singleton之後會立即被其他線程看到),並且禁止對其進行指令重排序優化(禁止指令重排優化這條語義直到jdk1.5以後才能正確工作。此前的JDK中即使將變量聲明爲volatile也無法完全避免重排序所導致的問題。所以,在jdk1.5版本前,雙重檢查鎖形式的單例模式是無法保證線程安全的。)。

這樣就可以保證在多線程中也只會創建一個對象,但同步鎖是比較耗費資源的,效率太低,是同步運行的,下個線程想要取得對象,就必須要等上一個線程釋放,纔可以繼續執行。所以說,在單例中添加同步鎖的方法比較適用於對對象獲取不是很頻繁地情況。

啊啊啊,又想保證安全,又想效率高點,我該腫麼做?

(4)DCL雙重檢查鎖機制:

public class Singleton {
    private Singleton(){}
    private static volatile Singleton singleton = null;
    public static Singleton getSingleton(){
        if(singleton == null){
            synchronized (Singleton.class){
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }    
}

這種寫法被稱爲“雙重檢查鎖”,顧名思義,就是在getSingleton()方法中,進行兩次null檢查。看似多此一舉,但實際上卻極大提升了併發度,進而提升了性能。爲什麼可以提高併發度呢?就像上文說的,在單例中new的情況非常少,絕大多數都是可以並行的讀操作。因此在加鎖前多進行一次null檢查就可以減少絕大多數的加鎖操作,執行效率提高的目的也就達到了。

雙重檢查鎖?好高大上呀!聽說還有更優雅的實現方式?

(5)枚舉寫法

public enum Singleton {
    INSTANCE;
    private String name;
    public String getName(){
        return name;
    }
    public void setName(String name){
        this.name = name;
    }
}

使用枚舉除了線程安全和防止反射強行調用構造器之外,還提供了自動序列化機制,防止反序列化的時候創建新的對象。因此,Effective Java推薦儘可能地使用枚舉來實現單例,但是在Android平臺上卻是不被推薦的。在這篇Android Training中明確指出:Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.。

有木有更牛X的實現方式呢?有滴有滴!

(6)靜態內部類的單例模式(也屬於餓漢式) 推薦!

 

public class Singleton {
    private Singleton(){}
    private static class Holder {
        private static Singleton singleton = new Singleton();
    }
    public static Singleton getSingleton(){
        return Holder.singleton;
    }
}

用內部類來保護單例,當Singleton類被加載時,內部類不會被初始化,所以可以確保Singleton類被載入JVM時,不會初始化單例類,當getSingleton()方法被調用時,纔會加載Holder,從而初始化singleton,同時,由於實例的建立是在類加載時完成的,故天生對多線程友好,getSingleton()方法也不需要使用synchronized修飾,因此,這種實現能兼顧延遲加載,非同步兩種優點。

注:在極端情況下,序列化和反序列化可能會破壞單例,一般來說不多見,如果存在就要多加註意,此時可以加入以下代碼:

private Object readResolve() {
     return Holder.singleton;
}

那麼readResolve()方法到底是何方神聖,其實當JVM從內存中反序列化地”組裝”一個新對象時,就會自動調用這個 readResolve()方法來返回我們指定好的對象了, 單例規則也就得到了保證。readResolve()的出現允許程序員自行控制通過反序列化得到的對象。

最後感謝各位大大寫的資料,參考如下:

淺談單例設計模式的幾種實現方式

設計模式之單例模式

Java實現單例的5種方式

你真的會寫單例模式嗎——Java實現

Java四種單例設計模式

設計模式--單例模式

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章