設計模式(4)——單例模式的學習及其六大戰將

長歌懷采薇發表於2020-09-27

單例模式的引發的陳年回憶

  記著N年以前,那時候還在上大學,有一門科目叫做軟體體系結構,教我們的老師是個40歲左右的女老師,姓韓,好像是東北大學博士畢業的,之所以對她還有些印象,那是因為初戀女友Y同學是被保送到東北大學讀研的,而當時韓老師和Y同學的師生關係好像還挺不錯,當然也是Y同學確定保送東北大學讀研的那一年,我們和平而堅決地分開了。。

  我就讀的大學是一所普通本科,我們學院一個年級可以保送的名額也僅僅只有三個,比起985 211那成堆的保送名額真的差的太遠,Y同學是相當優秀的,大學四年,每次期末成績必然是專業第一,而我,雖然一直在角落裡面默默努力著,可成績總是不盡人意,中游水平,也是因為某次關鍵的考試少考了一名,錯過了成為黨員的機會。。。。

  後來我也加入了考研大軍,雖然最終成績比國家線高了50多分,可是距離我的目標院校還少了10分左右,所以我又輸了。。。。

  於是在調劑,二戰,工作之間,,糾結半天,選擇了我最無奈但是方向最明確的一個:工作。畢竟,我父母年紀不小了,是時候賺錢了。。

  我大學時技術不好,或者說大部分人普遍技術都不好,雖然是軟體工程專業的,但是實踐機會太少,大部分時間都在上那些無聊的理論課,有些理論課,可以無聊到讓全班大部分人睡著或者上課玩手機,比如離散數學,計算機組成原理。。 

  而軟體體系結構這門課,算是無聊中的理論課相對有點激情的科目,因為,韓老師每次上課前都提問上節課的知識點。。。還會慷慨激昂的揮斥方遒指點江山,鼓勵我們考研考研!!這門課就是給我們講解設計模式的,不過,那些設計模式具體實現我大多忘卻了,只記著一些名字:工廠模式,抽象工廠模式,橋接模式,介面卡模式,命令模式,建造者模式,觀察者模式,迭代器模式,策略模式....有一個很有趣的模式我卻還記著比較清楚:單例模式。因為當時分為懶漢式和餓漢式,自己感覺很有趣(那時的快樂就那麼簡單),而且找工作時也有面試官讓我手寫單例模式(幸虧寫出來了)。。 

  最近隨意點開了一篇部落格,講單例模式的,原本沒想仔細看,然而卻發現,單例模式居然有六種實現模式,而我知道的兩種僅僅是最簡單的兩種,突然感覺自己很無知。。(啪啪打臉)

 於是乎,自己拜讀一番,然後寫了點demo。。今天也是打算正兒八經聊聊單例模式的。。不過有些觸景生情,一不小心扯遠了。

 好吧,現在我們言歸正傳,開始聊單例模式和它的六種實現。

 再稍等下,我想說,當年軟體體系結構這門課也是考了89的人,不高但是也不低啊,嘻嘻。

單例模式--從私有化無參建構函式開始

  我不知道該怎麼陳述我的話語,以前寫部落格都是大擺理論知識加點自寫的demo程式碼,坦白講,連我自己都不想看,於是想從這篇開始做出一些改變,用通俗的話語來講技術理論講明白,第一次嘗試這種風格,還請觀眾給點勇氣,多多鼓掌。

  所以單例模式,從字面上來講,那就是單個例項的模式,只創造一個例項。我們想一下哈,平時工作寫程式碼時,是不是動不動就new一下,動不動就給一個實體創造一個物件,那樣的話我們就可以針對某個實體創造多個了例項了,我們都明白,在新建一個實體後,即使不宣告構造方法java也是會預設的提供一個無參構造方法的,而我們平時new 例項也就是通過這個無參構造方法實現的,現在將這個無參構造方法宣告為私有的,那麼就不允許外部去建立了,舉個例子。

Zae z1 = new Zae();
Zae z2 = new Zae();
Zae z3 = new Zae();

  

  這種未宣告私有時,可以建立了3個例項z1,z2,z3哎,但是你一旦在實體中加入:private Zae(){}。 那麼你再這樣寫,保準紅色波浪線等著你,編譯期就給你亮紅燈。


  那麼我們又有疑問了,單例模式既然是不允許創造多個例項,但是允許創造一個例項啊,你這樣不就一個例項也創造不出來麼,你該怎麼樣擋住悠悠之口。


  接下來,將是小z給你講一下實現單例的方式了,在講之前,先給大家看一段最簡單的單例模式實現程式碼:

public class SingletonBeanHunger {

private SingletonBeanHunger(){}

private static SingletonBeanHunger singletonBeanHunger = new SingletonBeanHunger();

public static SingletonBeanHunger getInstance(){
    return singletonBeanHunger;
}

public void draw(){
    System.out.println("hunger-singleton");
}
}

  

  有沒有看到,有沒有看到,無參建構函式率先被宣告私有的了,而建立的那唯一各一個單例也是被宣告私有的了,只是提供了一個公有的呼叫返回方法。可能你此刻有點蒙,小Z你剛剛說java會預設提供一個無參構造方法,但是你現在宣告的這個私有的無參構造方法明明是你自己寫的,和系統提供的有什麼關係?哈哈,那是因為java的一種預設機制了,當沒有定義構造方法時每個類裡都有一個預設的無參的構造方法,此時該類就只有一個構造方法;而當你顯示的去定義類的構造方法時,那就沒有那個預設的構造方法了,該類所有的構造方法就是定義了的那些構造方法。  

  有心人發現,為什麼要將建立單例的程式碼變數宣告為static靜態的方法,這就是高明之處,也是重點了,因為類的載入機制,大家都明白變數裡面有靜態變數和成員變數的,而靜態變數它的大哥是類(Class),類建立它就建立,類銷燬它就銷燬,而成員變數的大哥是物件,也就是那個所謂的例項,物件建立出來它就建立,物件銷燬它也銷燬。因此呢,類指定只有一個的,也就是隻會被載入一次,那麼由此引發的靜態變數也就建立出這麼獨一份了,進而起到了單例的效果。而提供公共的呼叫方法getInstance(),這個方法時將類載入時建立的那個單例返回出去,因此別人想使用這個單例時,拿到的也就是獨一份的了,地址也是相同的。至於那個微不足道的draw()方法,這不是重點,因為它是個成員方法,也就只有物件可以呼叫了,這個是用來測試我呼叫getInstance()時拿到的是SingletonBeanHunger這個類的物件而已。


  通過上述的講解大家也差不多的明白單例的概念以及簡單實現邏輯,記住這幾點:1、單例只能有一個例項。2、單例類必須自己建立自己的唯一例項。3、單例類必須給所有其他物件提供這一例項。其實單例模式吧它主要是解決一個全域性使用的類頻繁的建立和銷燬的,畢竟有時太過頻繁建立銷燬例項,對JVM也是一個極大的挑戰,所以當我們想控制例項的數目和節省系統資源時,不妨考慮下單例模式。他雖然優點不少,但是也有些不可避免的缺點,無參構造都被私有化了,那麼當然不能被繼承了,也就沒有啥介面了,否則就和單一職責原則衝突了。



單例模式和它的六大戰將

  其實大家看到這個標題還是有點蒙的,六大戰將是什麼鬼,你怎麼不說是四大天王呢,你咋不上天呢。(汗。。)其實六大戰將就是六種實現方式了,因為最近在看部網路水文,其中就講到歐洲第一殺手“皇帝”和他的八大戰將,所以就想到了六大戰將了,哈哈哈,我真乃人才也。。(還要不要臉。。)

 

 

 

戰將一:伯爵(懶漢式-執行緒不安全)

這種方式是最基本的實現方式,這種實現最大的問題就是不支援多執行緒。因為沒有加鎖 synchronized,所以嚴格意義上它並不算單例模式。這種方式 lazy loading(延遲載入) 很明顯,不要求執行緒安全,在多執行緒不能正常工作。

/**
 * 懶漢式載入-執行緒不安全
 */
public class SingletonBeanLazyDg {

    private SingletonBeanLazyDg(){}

    private static SingletonBeanLazyDg singletonBeanLazyDg;

    public static  SingletonBeanLazyDg getInstance(){
        singletonBeanLazyDg = new SingletonBeanLazyDg();
        return singletonBeanLazyDg;
    }

    public void draw(){
        System.out.println("lazy-singleton-dangerous");
    }
}

 

戰將二:耶穌(懶漢式-執行緒安全)

這種方式具備很好的 lazy loading(延遲載入),能夠在多執行緒中很好的工作,但是,效率很低,99% 情況下不需要同步。

/**
 * 懶漢式載入-執行緒安全
 */
public class SingletonBeanLazy {

    private SingletonBeanLazy (){}

    private static SingletonBeanLazy singletonBeanLazy;

    public static synchronized SingletonBeanLazy getInstance(){
        if(singletonBeanLazy==null){
            singletonBeanLazy = new SingletonBeanLazy();
        }
        return singletonBeanLazy;
    }

    public void draw(){
        System.out.println("lazy-singleton");
    }
}

 

戰將三:野獸(餓漢模式)

這種方式沒有加鎖,執行效率會提高。但是類載入時就初始化,容易產生垃圾物件,浪費記憶體。 它基於 classloader 機制避免了多執行緒的同步問題,不過,instance 在類裝載時就例項化,雖然導致類裝載的原因有很多種,在單例模式中大多數都是呼叫 getInstance 方法, 但是也不能確定有其他的方式(或者其他的靜態方法)導致類裝載,這時候初始化 instance 顯然沒有達到 lazy loading (延遲載入)的效果。

/**
 * 餓漢式
 */
public class SingletonBeanHunger {

    private SingletonBeanHunger(){}

    private static SingletonBeanHunger singletonBeanHunger = new SingletonBeanHunger();

    public static SingletonBeanHunger getInstance(){
        return singletonBeanHunger;
    }

    public void draw(){
        System.out.println("hunger-singleton");
    }
}

 

戰將四:鬼影(雙檢鎖/雙重校驗鎖(DCL,即 double-checked locking))

這種方式採用雙鎖機制,安全且在多執行緒情況下能保持高效能。

/**
 * 雙重校驗鎖/雙檢鎖
 */
public class SingletonBeanDCL {

    private SingletonBeanDCL(){}

    private static volatile SingletonBeanDCL singletonBeanDCL;

    public static SingletonBeanDCL getInstance(){
        if(singletonBeanDCL==null){
            synchronized (SingletonBeanDCL.class){
                if (singletonBeanDCL == null){
                    singletonBeanDCL = new SingletonBeanDCL();
                }
            }
        }
        return singletonBeanDCL;
    }

    public void draw(){
        System.out.println("DCL-singleton");
    }
}

 

戰將五:金童(登記式/靜態內部類)

這種方式能達到雙檢鎖方式一樣的功效,但實現更簡單。對靜態域使用延遲初始化,應使用這種方式而不是雙檢鎖方式。這種方式只適用於靜態域的情況,雙檢鎖方式可在例項域需要延遲初始化時使用。

/**
 * 登記式/靜態內部類
 */
public class SingletonBeanStatic {

    private SingletonBeanStatic(){}

    private static class  SingletonBeanHolder{
        private static final SingletonBeanStatic SINGLETON_BEAN_STATIC = new SingletonBeanStatic();
    }

    public static final SingletonBeanStatic getInstance(){
        return SingletonBeanHolder.SINGLETON_BEAN_STATIC;
    }

    public void draw(){
        System.out.println("登記式/靜態內部類");
    }
    
}

 

戰將六:玉女(列舉)

雖然沒被廣泛應用,但是這是實現單例模式的最佳方法。它更簡潔,自動支援序列化機制,絕對防止多次例項化。 這種方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不僅能避免多執行緒同步問題,而且還自動支援序列化機制,防止反序列化重新建立新的物件,絕對防止多次例項化。

/**
 * 列舉
 */
public enum SingletonBeanEnum {
    INSTANCE;
    public void draw(){
        System.out.println("列舉式單例");
    }
}

 

大Boss:皇帝(Main-單例)

我只是一個無辜的測試類

/**
 * 測試單例六種實現
 */
public class TestSingle {
    public static void main(String[] args) {
        //懶漢式-執行緒不安全
        SingletonBeanLazyDg singletonBeanLazyDg = SingletonBeanLazyDg.getInstance();
        singletonBeanLazyDg.draw();
        
        //餓漢式-執行緒安全
        SingletonBeanHunger singletonBeanHunger = SingletonBeanHunger.getInstance();
        singletonBeanHunger.draw();

        //懶漢式
        SingletonBeanLazy singletonBeanLazy = SingletonBeanLazy.getInstance();
        singletonBeanLazy.draw();

        //雙檢鎖/雙重鎖校驗
        SingletonBeanDCL singletonBeanDCL = SingletonBeanDCL.getInstance();
        singletonBeanDCL.draw();

        //登記式/靜態內部類
        SingletonBeanStatic singletonBeanStatic = SingletonBeanStatic.getInstance();
        singletonBeanStatic.draw();

        //列舉
        SingletonBeanEnum singletonBeanEnum = SingletonBeanEnum.INSTANCE;
        singletonBeanEnum.draw();
    }
}

 

獐死於麝 鹿死於角
危險和榮譽總是成正比噠
請大家多多批評指教哈

相關文章