設(shè)計(jì)模式之單例模式

一肄渗、寫在前面

在Java眾多的設(shè)計(jì)模式當(dāng)中后雷,單例模式算是用的最多的設(shè)計(jì)模式之一了季惯,接下來(lái)我將介紹一下市面上常用的幾種寫法以及各自的優(yōu)缺點(diǎn)分析,廢話不多說(shuō)直接進(jìn)入正題臀突。

二勉抓、單例模式常見寫法

眾所周知“單例”顧名思義就是一個(gè)類只能有一個(gè)實(shí)例,就好比一個(gè)國(guó)家只能有一個(gè)政府一樣候学,單例模式在寫法上基本都有一個(gè)共同點(diǎn):通過私有化構(gòu)造器從而屏蔽掉外部類創(chuàng)建實(shí)例的可能(排除通過反射和反序列化操作)藕筋,下面我就通過政府Government這個(gè)類來(lái)實(shí)現(xiàn)幾種不同的單例模式。

2.1 餓漢式

餓漢式即無(wú)論程序后面用不用的到這個(gè)實(shí)例梳码,它都會(huì)在加載該類的時(shí)候就創(chuàng)建一個(gè)實(shí)例隐圾。然后通過一個(gè)getInstance()的靜態(tài)方法來(lái)獲取該實(shí)例。下面是具體實(shí)現(xiàn)邏輯:

public class Government {
    private static Government INSTANCE = new Government();

    private Government() {
    }

    public static Government getInstance() {
        return INSTANCE;
    }
}

優(yōu)點(diǎn):
(1)餓漢式是典型的空間換時(shí)間掰茶,當(dāng)類裝載的時(shí)候就會(huì)創(chuàng)建類實(shí)例暇藏,不管你用不用,先創(chuàng)建出來(lái)濒蒋,然后每次調(diào)用的時(shí)候盐碱,就不需要再判斷了,節(jié)省了運(yùn)行時(shí)間。
(2) 餓漢式是線程安全的瓮顽,因?yàn)樘摂M機(jī)保證只會(huì)裝載一次县好,在裝載類的時(shí)候是不會(huì)發(fā)生并發(fā)的。
缺點(diǎn):
(1)在沒用到的時(shí)候就創(chuàng)建出實(shí)例趣倾,導(dǎo)致浪費(fèi)內(nèi)存空間聘惦。
(2)在使用反射和反序列話時(shí)候會(huì)導(dǎo)致創(chuàng)建出多個(gè)實(shí)例的問題。例如以下測(cè)試代碼你就會(huì)發(fā)現(xiàn)該問題儒恋。(Government類需要實(shí)現(xiàn)Serializable接口)

       public static void main(String[] args) {
        Government instance1 = Government.getInstance();
        Government instance2 = Government.getInstance();
        System.out.println(instance1);
        System.out.println(instance2);

        try {
            //通過反射獲取實(shí)例3
            Class<Government> governmentClass1 = (Class<Government>) Class.forName("com.example.singlemode.Government");
            Constructor<Government> constructor1 = governmentClass1.getDeclaredConstructor(null);
            constructor1.setAccessible(true);
            Government instance3 = constructor1.newInstance();

            //通過反射獲取實(shí)例4
            Class<Government> governmentClass2 = (Class<Government>) Class.forName("com.example.singlemode.Government");
            Constructor<Government> constructor2 = governmentClass2.getDeclaredConstructor(null);
            constructor2.setAccessible(true);
            Government instance4 = constructor2.newInstance();

            System.out.println(instance3);
            System.out.println(instance4);

            //通過反序列化獲取實(shí)例對(duì)象
            //1.將instance1寫入到磁盤
            FileOutputStream fos = new FileOutputStream("object.out");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(instance1);
            oos.close();

            // 2. 把硬盤文件上的對(duì)象讀出來(lái)
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.out"));
            Government instance5 = (Government) ois.readObject();
            ois.close();

            System.out.println(instance5);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

執(zhí)行結(jié)果:

com.example.singlemode.Government@511d50c0
com.example.singlemode.Government@511d50c0
com.example.singlemode.Government@60e53b93
com.example.singlemode.Government@5e2de80c
com.example.singlemode.Government@6f496d9f

你會(huì)發(fā)現(xiàn)1善绎,2通過getInstance()方法獲取的實(shí)例相同,3诫尽,4通過反射獲取到的實(shí)例不同禀酱,這就是反射導(dǎo)致的獲取多個(gè)實(shí)例的問題,5通過將1的實(shí)例寫入磁盤在反序列化獲取的實(shí)例和1之前的也不同牧嫉,這就是反序列化導(dǎo)致的獲取到不同實(shí)例的問題剂跟。

針對(duì)這兩個(gè)問題可以采取以下方式避免,代碼如下:


public class Government implements Serializable{
    private static Government INSTANCE = new Government();

    private Government() {
        //解決反射問題酣藻,一旦通過反射調(diào)用構(gòu)造器創(chuàng)建對(duì)象后就拋出異常
        if (null != INSTANCE) {
            throw new RuntimeException();
        }
    }

    public static Government getInstance() {
        return INSTANCE;
    }

    // 防止反序列化獲取多個(gè)對(duì)象的漏洞曹洽。
    // 無(wú)論是實(shí)現(xiàn)Serializable接口,或是Externalizable接口辽剧,當(dāng)從I/O流中讀取對(duì)象時(shí)送淆,readResolve()方法都會(huì)被調(diào)用到。
    // 實(shí)際上就是用readResolve()中返回的對(duì)象直接替換在反序列化過程中創(chuàng)建的對(duì)象怕轿。
    private Object readResolve() throws ObjectStreamException {
        return INSTANCE;
    }
}

再次運(yùn)行之前的代碼會(huì)發(fā)現(xiàn)執(zhí)行到反射獲取對(duì)象的地方時(shí)候回拋出異常:

解決反射導(dǎo)致多實(shí)例問題.png

先將反射獲取對(duì)象的代碼注釋掉偷崩,然后看反序列化運(yùn)行的結(jié)果:

com.example.singlemode.Government@511d50c0
com.example.singlemode.Government@511d50c0
com.example.singlemode.Government@511d50c0

最后一個(gè)是反序列化的,發(fā)現(xiàn)和之前的是同一個(gè)實(shí)例撞羽。這就說(shuō)明解決了這兩個(gè)問題阐斜。

2.2 懶漢式

和餓漢式不同之處就在于屎鳍,它是采用懶加載在用到的時(shí)候才會(huì)去創(chuàng)建,代碼實(shí)現(xiàn)如下:

public class Government {
   private static Government INSTANCE;

   private Government() {
   }

   public static Government getInstance() {
       if (INSTANCE == null) {
           INSTANCE = new Government();
       }
       return INSTANCE;
   }

}

優(yōu)點(diǎn):
(1) 如果一直沒有使用的話萨蚕,那就不會(huì)創(chuàng)建實(shí)例试幽,節(jié)約內(nèi)存空間
缺點(diǎn):
(1)每次獲取實(shí)例都會(huì)進(jìn)行判斷综苔,看是否需要?jiǎng)?chuàng)建實(shí)例扛伍,浪費(fèi)判斷的時(shí)間稚伍。
(2)從線程安全性上講床三,不加同步的懶漢式是線程不安全的酥泛,比如惕澎,有兩個(gè)線程莉测,一個(gè)是線程A,一個(gè)是線程B唧喉,它們同時(shí)調(diào)用getInstance方法捣卤,那就可能導(dǎo)致并發(fā)問題忍抽。
(3)在使用反射和反序列話時(shí)候會(huì)導(dǎo)致創(chuàng)建出多個(gè)實(shí)例的問題。解決方式和之前餓漢式的相似這里不做過多說(shuō)明董朝。

2.3 DoubleCheck式

DoubleCheck式也叫雙重檢查式鸠项,主要是進(jìn)行了兩重判空操作,同時(shí)增加了同步鎖具體實(shí)現(xiàn)如下:


public class Government implements Serializable{
    private volatile static Government INSTANCE;

    private Government() {
    }

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

優(yōu)點(diǎn):
(1) 一直沒有使用的話,那就不會(huì)創(chuàng)建實(shí)例子姜,節(jié)約內(nèi)存空間
(2)加了同步鎖祟绊,線程安全
缺點(diǎn):
(1)每次獲取實(shí)例都會(huì)進(jìn)行判斷,第一次會(huì)持有同步鎖,浪費(fèi)判斷的時(shí)間和性能損耗哥捕。
(2)在使用反射和反序列話時(shí)候會(huì)導(dǎo)致創(chuàng)建出多個(gè)實(shí)例的問題牧抽。解決方式和之前餓漢式的相似這里不做過多說(shuō)明。

2.4 靜態(tài)內(nèi)部類式

靜態(tài)內(nèi)部類實(shí)現(xiàn)方式(也是一種懶加載方式)遥赚,具體實(shí)現(xiàn)如下:


public class Government implements Serializable{

    private static class GovernmentHolder{
        private static final Government INSTANCE = new Government();
    }

    private Government() {
    }

    public static Government getInstance() {
        return GovernmentHolder.INSTANCE;
    }
}

優(yōu)點(diǎn):
(1) 一直沒有使用的話扬舒,那就不會(huì)創(chuàng)建實(shí)例,節(jié)約內(nèi)存空間
(2)采用靜態(tài)內(nèi)部類凫佛,線程安全
缺點(diǎn):
(1)每次獲取實(shí)例都會(huì)進(jìn)行判斷和持有同步鎖讲坎,浪費(fèi)判斷的時(shí)間和性能損耗。
(2)在使用反射和反序列話時(shí)候會(huì)導(dǎo)致創(chuàng)建出多個(gè)實(shí)例的問題愧薛。解決方式和之前餓漢式的相似這里不做過多說(shuō)明晨炕。

2.5 枚舉式

枚舉式絕對(duì)是這五種里堪稱最完美的模式,簡(jiǎn)簡(jiǎn)單單的一點(diǎn)代碼就實(shí)現(xiàn)了一個(gè)線程安全毫炉,lazy loading的單例瓮栗,與其說(shuō)是寫法鬼斧神工,不如說(shuō)是恰如其分地應(yīng)用了enum的性質(zhì)碘箍。

public enum  Government {
    INSTANCE
}

首先遵馆,我們都知道enum是由class實(shí)現(xiàn)的鲸郊,換言之丰榴,enum可以實(shí)現(xiàn)很多class的內(nèi)容,包括可以有member和member function秆撮,這也是我們可以用enum作為一個(gè)類來(lái)實(shí)現(xiàn)單例的基礎(chǔ)四濒。另外,由于enum是通過繼承了Enum類實(shí)現(xiàn)的职辨,enum結(jié)構(gòu)不能夠作為子類繼承其他類盗蟆,但是可以用來(lái)實(shí)現(xiàn)接口。此外舒裤,enum類也不能夠被繼承喳资,在反編譯中,我們會(huì)發(fā)現(xiàn)該類是final的腾供。
其次仆邓,enum有且僅有private的構(gòu)造器鲜滩,防止外部的額外構(gòu)造,這恰好和單例模式吻合节值,也為保證單例性做了一個(gè)鋪墊徙硅。這里展開說(shuō)下這個(gè)private構(gòu)造器,如果我們不去手寫構(gòu)造器搞疗,則會(huì)有一個(gè)默認(rèn)的空參構(gòu)造器嗓蘑,我們也可以通過給枚舉變量參量來(lái)實(shí)現(xiàn)類的初始化,例如:

public enum Color{
    RED(1),GREEN(2),BLUE(3);
    private int code;
    Color(int code){
        this.code=code;
    }
    public int getCode(){
        return code;
    }
}

三、總結(jié)

單例模式的實(shí)現(xiàn)形式有多種匿乃,在使用的時(shí)候根據(jù)需要恰當(dāng)?shù)倪x擇實(shí)現(xiàn)形式桩皿。本文內(nèi)容算是自己對(duì)單例模式的一些認(rèn)識(shí)和理解,也是參考了一些前輩們的思路扳埂,這里向他們致謝业簿。如有錯(cuò)誤望大家指正。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末阳懂,一起剝皮案震驚了整個(gè)濱河市梅尤,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌岩调,老刑警劉巖巷燥,帶你破解...
    沈念sama閱讀 219,366評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異号枕,居然都是意外死亡缰揪,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門葱淳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)钝腺,“玉大人,你說(shuō)我怎么就攤上這事赞厕⊙藓” “怎么了?”我有些...
    開封第一講書人閱讀 165,689評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵皿桑,是天一觀的道長(zhǎng)毫目。 經(jīng)常有香客問我,道長(zhǎng)诲侮,這世上最難降的妖魔是什么镀虐? 我笑而不...
    開封第一講書人閱讀 58,925評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮沟绪,結(jié)果婚禮上刮便,老公的妹妹穿的比我還像新娘。我一直安慰自己绽慈,他們只是感情好恨旱,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評(píng)論 6 392
  • 文/花漫 我一把揭開白布抄肖。 她就那樣靜靜地躺著,像睡著了一般窖杀。 火紅的嫁衣襯著肌膚如雪漓摩。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,727評(píng)論 1 305
  • 那天入客,我揣著相機(jī)與錄音管毙,去河邊找鬼。 笑死桌硫,一個(gè)胖子當(dāng)著我的面吹牛夭咬,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播铆隘,決...
    沈念sama閱讀 40,447評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼卓舵,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了膀钠?” 一聲冷哼從身側(cè)響起掏湾,我...
    開封第一講書人閱讀 39,349評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎肿嘲,沒想到半個(gè)月后融击,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,820評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡雳窟,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評(píng)論 3 337
  • 正文 我和宋清朗相戀三年尊浪,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片封救。...
    茶點(diǎn)故事閱讀 40,127評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡拇涤,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出誉结,到底是詐尸還是另有隱情鹅士,我是刑警寧澤,帶...
    沈念sama閱讀 35,812評(píng)論 5 346
  • 正文 年R本政府宣布搓彻,位于F島的核電站如绸,受9級(jí)特大地震影響嘱朽,放射性物質(zhì)發(fā)生泄漏旭贬。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評(píng)論 3 331
  • 文/蒙蒙 一搪泳、第九天 我趴在偏房一處隱蔽的房頂上張望稀轨。 院中可真熱鬧,春花似錦岸军、人聲如沸奋刽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)佣谐。三九已至肚吏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間狭魂,已是汗流浹背罚攀。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留雌澄,地道東北人斋泄。 一個(gè)月前我還...
    沈念sama閱讀 48,388評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像镐牺,于是被迫代替她去往敵國(guó)和親炫掐。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容