從未這么明白的設計模式(一):單例模式

cover

什么是單例?為什么要用單例苍狰?

一個類被設計出來办龄,就代表它表示具有某種行為(方法),屬性(成員變量)淋昭,而一般情況下俐填,當我們想使用這個類時,會使用new關鍵字翔忽,這時候jvm會幫我們構造一個該類的實例英融。而我們知道,對于new這個關鍵字以及該實例歇式,相對而言是比較耗費資源的驶悟。所以如果我們能夠想辦法在jvm啟動時就new好,或者在某一次實例new好以后材失,以后不再需要這樣的動作痕鳍,就能夠節(jié)省很多資源了。

哪些類可以使用單例?

一般而言笼呆,我們總是希望無狀態(tài)的類能夠設計成單例熊响,那這個無狀態(tài)代表什么呢? 簡單而言诗赌,對于同一個實例汗茄,如果多個線程同時使用,并且不使用額外的線程同步手段铭若,不會出現線程同步的問題洪碳,我們就可以認為是無狀態(tài)的,再簡單點:一個類沒有成員變量奥喻,或者它的成員變量也是無狀態(tài)的偶宫,我們就可以考慮設計成單例。

實現方法

好了环鲤,我們已經知道什么是單例,為什么要使用單例了憎兽,那我們接下來繼續(xù)討論下怎么實現單例冷离。
一般來說,我們可以把單例分為行為上的單例管理上的單例纯命。行為上的單例代表不管如何操作(此處不談cloneable西剥,反射),至始至終jvm中都只有一個類的實例亿汞,而管理上的單例則可以理解為:不管誰去使用這個類瞭空,都要守一定的規(guī)矩,比方說疗我,我們使用某個類咆畏,只能從指定的地方’去拿‘,這樣拿到就是同一個類了吴裤。
而對于管理上的單例旧找,相信大家最為熟悉的就是spring了,spring將所有的類放到一個容器中麦牺,以后使用該類都從該容器去取钮蛛,這樣就保證了單例。
所以這里我們剩下的就是接著來談談如何實現行為上的單例了剖膳。一般來說魏颓,這種單例實現有兩種思路,私有構造器吱晒,枚舉甸饱。

枚舉實現單例

枚舉實現單例是最為推薦的一種方法,因為就算通過序列化枕荞,反射等也沒辦法破壞單例性柜候,例子:

public enum SingletonEnum {
    INSTANCE;

    public static void main(String[] args) {
        System.out.println(SingletonEnum.INSTANCE == SingletonEnum.INSTANCE);
    }
}

結果自然是true搞动,而如果我們嘗試使用反射破壞單例性:

public enum BadSingletonEnum {
    /**
     *
     */
    INSTANCE;

    public static void main(String[] args) throws Exception{
        System.out.println(BadSingletonEnum.INSTANCE == BadSingletonEnum.INSTANCE);

        Constructor<BadSingletonEnum> badSingletonEnumConstructor = BadSingletonEnum.class.getDeclaredConstructor();
        badSingletonEnumConstructor.setAccessible(true);
        BadSingletonEnum badSingletonEnum = badSingletonEnumConstructor.newInstance();

        System.out.println(BadSingletonEnum.INSTANCE == badSingletonEnum);
    }
}

結果如下:

Exception in thread "main" java.lang.NoSuchMethodException: cn.jsbintask.BadSingletonEnum.<init>()
    at java.lang.Class.getConstructor0(Class.java:3082)
    at java.lang.Class.getDeclaredConstructor(Class.java:2178)
    at cn.jsbintask.BadSingletonEnum.main(BadSingletonEnum.java:18)

異常居然是沒有init方法,這是為什么呢渣刷? 那我們反編譯查看下這個枚舉類的字節(jié)碼:

// class version 52.0 (52)
// access flags 0x4031
// signature Ljava/lang/Enum<Lcn/jsbintask/BadSingletonEnum;>;
// declaration: cn/jsbintask/BadSingletonEnum extends java.lang.Enum<cn.jsbintask.BadSingletonEnum>
public final enum cn/jsbintask/BadSingletonEnum extends java/lang/Enum {

  // compiled from: BadSingletonEnum.java

  // access flags 0x4019
  public final static enum Lcn/jsbintask/BadSingletonEnum; INSTANCE

  // access flags 0x101A
  private final static synthetic [Lcn/jsbintask/BadSingletonEnum; $VALUES
}

結果發(fā)現這個枚舉類繼承了抽象類java.lang.Enum鹦肿,我們接著看下Enum,發(fā)現構造器:

/**
    * Sole constructor.  Programmers cannot invoke this constructor.
    * It is for use by code emitted by the compiler in response to
    * enum type declarations.
    *
    * @param name - The name of this enum constant, which is the identifier
    *               used to declare it.
    * @param ordinal - The ordinal of this enumeration constant (its position
    *         in the enum declaration, where the initial constant is assigned
    *         an ordinal of zero).
*/
protected Enum(String name, int ordinal) {
    this.name = name;
    this.ordinal = ordinal;
}

那我們接著改變代碼辅柴,反射調用這個構造器:

public enum BadSingletonEnum {
    /**
     *
     */
    INSTANCE();

    public static void main(String[] args) throws Exception{
        System.out.println(BadSingletonEnum.INSTANCE == BadSingletonEnum.INSTANCE);

        Constructor<BadSingletonEnum> badSingletonEnumConstructor = BadSingletonEnum.class.getDeclaredConstructor(String.class, int.class);
        badSingletonEnumConstructor.setAccessible(true);
        BadSingletonEnum badSingletonEnum = badSingletonEnumConstructor.newInstance("test", 0);

        System.out.println(BadSingletonEnum.INSTANCE == badSingletonEnum);
    }
}

結果如下:

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    at cn.jsbintask.BadSingletonEnum.main(BadSingletonEnum.java:21)

這次雖然方法找到了箩溃,但是直接給我們了一句Cannot reflectively create enum objects,不能夠反射創(chuàng)造枚舉對象碌嘀,接著我們繼續(xù)看下newInstance(...)這個方法:

public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

關鍵代碼就是:if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects");涣旨,所以就是jdk從根本上拒絕了使用反射去創(chuàng)建(知道為啥java推薦使用enum實現單例了吧),另外股冗,我們再觀察下Enum類的clone和序列化方法霹陡,如下:

protected final Object clone() throws CloneNotSupportedException {
    throw new CloneNotSupportedException();
}

private void readObject(ObjectInputStream in) throws IOException,
    ClassNotFoundException {
    throw new InvalidObjectException("can't deserialize enum");
}

private void readObjectNoData() throws ObjectStreamException {
    throw new InvalidObjectException("can't deserialize enum");
}

一眼看出,直接丟出異常止状,不允許這么做E朊蕖(真親兒子系列)
所以怯疤,結論就是:枚舉是最靠譜的實現單例的方式浆洗!

私有構造器

另外一個實現單例最普通的方法則是私有構造器,開放獲取實例公共方法集峦,雖然這種方法還是可以用clone伏社,序列化,反射破壞單例性(除非特殊情況塔淤,我們不會這么做)摘昌,但是卻是最容易理解使用的。而這種方式又分了飽漢式凯沪,餓漢式第焰。

餓漢式

看名字就知道,饑渴7谅怼(咳咳挺举,開個玩笑),它指的是當一個類被jvm加載的時候就會被實例化烘跺,這樣可以從根本上解決多個線程的同步問題湘纵,例子如下:

public class FullSingleton {
    private static FullSingleton ourInstance = new FullSingleton();

    public static FullSingleton getInstance() {
        return ourInstance;
    }

    private FullSingleton() {
    }

    public static void main(String[] args) {
        System.out.println(FullSingleton.getInstance() == FullSingleton.getInstance());
    }
}

結果自然是true,雖然這種做法很方便的幫我們解決了多線程實例化的問題滤淳,但是缺點也很明顯梧喷,因為這句代碼private static FullSingleton ourInstance = new FullSingleton();的關系,所以該類一旦被jvm加載就會馬上實例化,那如果我們不想用這個類怎么辦呢铺敌? 是不是就浪費了呢汇歹?既然這樣,我們來看下替代方案偿凭! 飽漢式产弹。

飽漢式

既然是,就代表它不著急弯囊,那我們可以這么寫:

public class HungryUnsafeSingleton {
    private static HungryUnsafeSingleton instance;
    
    public static HungryUnsafeSingleton getInstance() {
        if (instance == null) {
            instance = new HungryUnsafeSingleton();
        }
        
        return instance;
    }
    
    private HungryUnsafeSingleton() {}
}

用意很容易理解痰哨,就是用到getInstance()方法才去檢查instance,如果為null匾嘱,就new一個斤斧,這樣就不怕浪費了,但是這個時候問題就來了:現在有這么一種情況霎烙,在有兩個線程同時 運行到了 instane == null這個語句撬讽,并且都通過了,那他們就會都實例化一個對象悬垃,這樣就又不是單例了锐秦。既然這樣,哪有什么解決辦法呢盗忱? 鎖方法

  1. 直接同步方法
    這種方法比較干脆利落,那就是直接在getInstance()方法上加鎖羊赵,這樣就解決了線程問題:
public class HungrySafeSingleton {
    private static HungrySafeSingleton instance;

    public static synchronized HungrySafeSingleton getInstance() {
        if (instance == null) {
            instance = new HungrySafeSingleton();
        }

        return instance;
    }

    private HungrySafeSingleton() {
        System.out.println("HungryUnsafeSingleton.HungryUnsafeSingleton");
    }

    public static void main(String[] args) {
        System.out.println(HungrySafeSingleton.getInstance() == HungrySafeSingleton.getInstance());
    }
}

很簡單趟佃,很容易理解,加鎖昧捷,只有一個線程能實例該對象闲昭。但是,此時問題又來了靡挥,我們知道對于靜態(tài)方法而言序矩,synchronized關鍵字會鎖住整個 Class,這時候又會有性能問題了(尼瑪墨跡)跋破,那有沒有優(yōu)化的辦法呢簸淀? 雙重檢查鎖

public class HungrySafeSingleton {
    private static volatile HungrySafeSingleton instance;

    public static HungrySafeSingleton getInstance() {
        /* 使用一個本地變量可以提高性能 */
        HungrySafeSingleton result = instance;

        if (result == null) {

            synchronized (HungrySafeSingleton.class) {

                result = instance;
                if (result == null) {
                    instance = result = new HungrySafeSingleton();
                }
            }
        }

        return result;
    }

    private HungrySafeSingleton() {
        System.out.println("HungryUnsafeSingleton.HungryUnsafeSingleton");
    }

    public static void main(String[] args) {
        System.out.println(HungrySafeSingleton.getInstance() == HungrySafeSingleton.getInstance());
    }
}

用意也很明顯,synchronized關鍵字只加在了關鍵的地方毒返,并且通過本地變量提高了性能(effective java)租幕,這樣線程安全并且不浪費資源的單例就完成了。

總結

本章拧簸,我們一步一步從什么是單例劲绪,到為什么要使用單例,再到怎么使用單例,并且從源碼角度分析了為什么枚舉是最適合的實現方式贾富,然后接著講解了飽漢式歉眷,餓漢式的寫法以及好處,缺點颤枪。
例子源碼:https://github.com/jsbintask22/design-pattern-learning.git
本文原創(chuàng)地址:https://jsbintask.cn/2019/01/29/designpattern/singleton/汗捡,轉載請注明出處。
如果你覺得有用汇鞭,歡迎關注凉唐,分享!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末霍骄,一起剝皮案震驚了整個濱河市台囱,隨后出現的幾起案子,更是在濱河造成了極大的恐慌读整,老刑警劉巖簿训,帶你破解...
    沈念sama閱讀 222,627評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異米间,居然都是意外死亡强品,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 95,180評論 3 399
  • 文/潘曉璐 我一進店門屈糊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來的榛,“玉大人,你說我怎么就攤上這事逻锐》蛏危” “怎么了?”我有些...
    開封第一講書人閱讀 169,346評論 0 362
  • 文/不壞的土叔 我叫張陵昧诱,是天一觀的道長晓淀。 經常有香客問我,道長盏档,這世上最難降的妖魔是什么凶掰? 我笑而不...
    開封第一講書人閱讀 60,097評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮蜈亩,結果婚禮上懦窘,老公的妹妹穿的比我還像新娘。我一直安慰自己勺拣,他們只是感情好奶赠,可當我...
    茶點故事閱讀 69,100評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著药有,像睡著了一般毅戈。 火紅的嫁衣襯著肌膚如雪苹丸。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,696評論 1 312
  • 那天苇经,我揣著相機與錄音赘理,去河邊找鬼。 笑死扇单,一個胖子當著我的面吹牛商模,可吹牛的內容都是我干的。 我是一名探鬼主播蜘澜,決...
    沈念sama閱讀 41,165評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼施流,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了鄙信?” 一聲冷哼從身側響起瞪醋,我...
    開封第一講書人閱讀 40,108評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎装诡,沒想到半個月后银受,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 46,646評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡鸦采,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,709評論 3 342
  • 正文 我和宋清朗相戀三年宾巍,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片渔伯。...
    茶點故事閱讀 40,861評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡顶霞,死狀恐怖,靈堂內的尸體忽然破棺而出锣吼,到底是詐尸還是另有隱情确丢,我是刑警寧澤,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布吐限,位于F島的核電站,受9級特大地震影響褂始,放射性物質發(fā)生泄漏诸典。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,196評論 3 336
  • 文/蒙蒙 一崎苗、第九天 我趴在偏房一處隱蔽的房頂上張望狐粱。 院中可真熱鬧,春花似錦胆数、人聲如沸肌蜻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,698評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蒋搜。三九已至篡撵,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間豆挽,已是汗流浹背育谬。 一陣腳步聲響...
    開封第一講書人閱讀 33,804評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留帮哈,地道東北人膛檀。 一個月前我還...
    沈念sama閱讀 49,287評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像娘侍,于是被迫代替她去往敵國和親咖刃。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,860評論 2 361

推薦閱讀更多精彩內容