Java設(shè)計模式——單例模式(Singleton pattern)


眾所周知,在代碼中采用合理的設(shè)計模式酵幕,不僅僅能使代碼更容易被他人理解扰藕,同時也能使整體模塊擁有更合理的結(jié)構(gòu),方便后期擴展維護芳撒。因此就產(chǎn)生了一些“套路”邓深,而這些“套路”我們便稱之為“設(shè)計模式”。

另外笔刹,如果想要弄明白一些知識芥备,一定要分清楚順序,即遇到了什么問題舌菜、要怎么解決以及有沒有更好的辦法萌壳,這樣帶著問題去思考,可以達到事半功倍的效果日月。

言歸正傳袱瓮,開始說單例模式。按照上面的思考順序爱咬,我們一步一步來分析尺借。

1. 有本參奏,無本退朝

開始上早朝了啊~平常我們在使用某個類的實例時精拟,直接使用關(guān)鍵字new燎斩,便可創(chuàng)建一個實例對象虱歪。但有時候可能會頻繁使用某個實例對象,或者創(chuàng)建這個對象比較耗費資源瘫里,例如請了一個管家实蔽,需要管家?guī)湍愀梢恍┦拢偛荒苊看涡枰芗业臅r候就重新聘請一個吧谨读?最好的方法就是長期聘請這個管家局装,需要的時候直接吩咐就行了。突然發(fā)現(xiàn)我這個例子舉得是很恰當袄椭场铐尚!

通過上面的闡述,我們遇到一個問題哆姻,那就是某個類的實例對象頻繁使用宣增,或者創(chuàng)建時比較費時費事時,希望只創(chuàng)建一次對象矛缨,并且一個就夠了(你要是非得請兩個管家爹脾,我只能說你有錢)。在這種情況下箕昭,我們來開始思考如果解決這個問題灵妨。

2. 建言獻策,百花齊放

大家都開始獻上良策啊落竹,一個一個來泌霍,第一位,趙學士你先發(fā)言~

2.1 餓漢式

其實挺好解決的述召,看我下面的代碼:

// 趙學士的方案
public class Singleton {
    private static Singleton sInstance = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return sInstance;
    }
}

構(gòu)造方法私有化這就不解釋了朱转,保證外部不能隨便通過new關(guān)鍵字來創(chuàng)建對象;靜態(tài)成員變量sIntanceSingleton這個類加載的時候就初始化积暖,創(chuàng)建了Singleton對象藤为,并且只存在一個;通過Singleton.getInstance()方法可以獲取該實例對象夺刑,這就是單例模式凉蜂!這就解決了問題啊同志們!

不過錢大臣想了想說性誉,不過這種方法好像有點弊端,假如我現(xiàn)在還不需要管家茎杂,總不能讓我白花錢養(yǎng)著吧错览?能不能在我需要的時候再花錢聘請管家?

誒~~你這么一說也有道理啊煌往,那錢大臣倾哺,說說你的辦法轧邪。

2.2 懶漢式

話不多說,先看代碼:

// 錢大臣的方案
public class Singleton {
    private static Singleton sInstance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (sInstance == null) {
            sInstance = new Singleton();
        }
        return sInstance;
    }
}

怎么樣羞海?這個辦法不錯吧忌愚!成員變量默認初始化不創(chuàng)建對象,當調(diào)用Singleton.getInstance()方法時却邓,如果sInstancenull再創(chuàng)建對象硕糊,否則就直接返回,保證了你的要求腊徙。

此時孫丞相“哼”了一下說简十,你這還不如趙學士呢!趙學士有可能提前白花錢聘請了一個管家撬腾,而你有可能多花錢請了好幾個管家呢螟蝙!你都沒有考慮到多線程的情況!錢大臣一聽趕緊做了修改民傻,代碼如下:

// 錢大臣的方案2
public class Singleton {
    private static Singleton sInstance;

    private Singleton() {
    }

    public synchronized static Singleton getInstance() {
        if (sInstance == null) {
            sInstance = new Singleton();
        }
        return sInstance;
    }
}

給getInstance方法加了關(guān)鍵字synchronized胰默,保證創(chuàng)建對象的時候只有一個調(diào)用者,可以了吧漓踢?孫丞相又“哼”了一聲牵署,可每次調(diào)用的時候都會因為這個鎖帶來的時間開銷,你以為開鎖不要時間芭砦怼碟刺?性能低下!錢大臣臉有點紅薯酝,于是又做了修改:

// 錢大臣的方案3
public class Singleton {
    private static Singleton sInstance;

    private Singleton() {
    }

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

sInstance = new Singleton();語句加了鎖半沽,應(yīng)該沒問題了吧?孫丞相第三次“哼”了一聲吴菠,我給你假設(shè)個情況啊者填,設(shè)現(xiàn)有線程A和B,在某個時刻兩個線程都通過了判空語句但都沒有取到鎖資源做葵,然后線程A先取得鎖資源進入臨界區(qū)(被鎖的代碼塊)占哟,創(chuàng)建了一個對象,然后退出臨界區(qū)酿矢,釋放鎖資源榨乎。接著線程B取得鎖資源進入臨界區(qū),開始創(chuàng)建對象瘫筐,退出臨界區(qū)蜜暑,釋放鎖資源,請問現(xiàn)在有幾個Sinleton對象策肝?錢大臣聽后說那我直接把鎖加到判空語句之前肛捍!

// 錢大臣的方案4
public class Singleton {
    private static Singleton sInstance;

    private Singleton() {
    }

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

孫丞相直接笑了隐绵,說你這樣和在方法上加synchronized關(guān)鍵字有什么區(qū)別。連續(xù)被懟拙毫,錢大臣感覺很沒面子直接反駁道依许,you can you up, no can no bb!

2.3 雙重校驗鎖DCL(double checked locking)

孫丞相大手一揮說道,看好了啊缀蹄,今兒讓我教教你怎么做人峭跳!

// 孫丞相的方案
public class Singleton {
    private static volatile Singleton sInstance;

    private Singleton() {
    }

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

首先,方法鎖改成代碼塊鎖袍患,減少鎖的范圍坦康;其次第一次判空,在單線程的情況下提升了效率诡延,但此時如果同時存在兩個線程并發(fā)情況滞欠,即都判空成功,接下來會由鎖內(nèi)的第二次判空來過濾肆良。還是剛才的例子筛璧,假設(shè)現(xiàn)有線程A和B,在某個時刻兩個線程都通過了第一次判空語句但都沒有取到鎖資源惹恃。然后線程A先取得鎖資源進入臨界區(qū)(被鎖的代碼塊)夭谤,執(zhí)行第二次判空語句,判空成功巫糙,創(chuàng)建了一個對象朗儒,然后退出臨界區(qū)厢绝,釋放鎖資源烟零。接著線程B取得鎖資源進入臨界區(qū),執(zhí)行判空語句發(fā)現(xiàn)不通過龙亲,直接退出臨界區(qū)浙值,釋放鎖資源恳不。

另外在成員變量sInstance前面加了一個volatile關(guān)鍵字,這個特別重要开呐。容我裝個逼:在Java內(nèi)存模型(JMM)中烟勋,并不限制處理器的指令順序,說白了就是在不影響結(jié)果的情況下筐付,順序可能會被打亂卵惦。

在執(zhí)行sInstance = new Singleton();這條命令語句時,JMM并不是一下就執(zhí)行完畢的瓦戚,即不是原子性鸵荠,實質(zhì)上這句命令分為三大部分:

  1. 為對象分配內(nèi)存
  2. 執(zhí)行構(gòu)造方法語句,初始化實例對象
  3. 把sInstance的引用指向分配的內(nèi)存空間

在JMM中這三個步驟中的2和3不一定是順序執(zhí)行的伤极,如果線程A執(zhí)行的順序為1蛹找、3、2哨坪,在第2步執(zhí)行完畢的時候庸疾,恰好線程B執(zhí)行第一次判空語句,則會直接返回sInstance当编,那么此時獲取到的sInstance僅僅只是不為null届慈,實質(zhì)上沒有初始化,這樣的對象肯定是有問題的忿偷!

volatile關(guān)鍵字的存在意義就是保證了執(zhí)行命令不會被重排序金顿,也就避免了這種異常情況的發(fā)生,所以這種獲取單例的方法才是真正的安全可靠鲤桥!

一直默默不做聲的李將軍冷不丁地開口了揍拆,孫丞相啊,你不覺得你這樣寫很麻煩嗎茶凳?我有更簡單的寫法呢嫂拴!

2.4 靜態(tài)內(nèi)部類實現(xiàn)的單例模式

你看你這又是判空又是加鎖的,多麻煩贮喧,其實可以通過靜態(tài)內(nèi)部類的方式筒狠,既保證了只存在一個單例,又保證了線程安全箱沦,代碼如下:

// 李將軍的方案
public class Singleton {

    private Singleton() {
    }

    public static Singleton getInstance() {
        return SingletonHolder.sInstance;
    }

    private static class SingletonHolder {
        private static Singleton sInstance = new Singleton();
    }
}

當外部類Singleton被加載時辩恼,其靜態(tài)內(nèi)部類SingeletonHolder不會被加載,所以它的成員變量sInstance是不會被初始化的谓形,只有當調(diào)用Singleton.getInstance()方法時灶伊,才會加載SingeletonHolder并且初始化其成員變量,而類加載時是線程安全的套耕,這樣既保證了延遲加載谁帕,也保證了線程安全,同時也簡化了代碼量冯袍,一舉三得匈挖!

2.5 枚舉單例

在說完上面4種單例模式的實現(xiàn)方式之后,不知道大家有沒有想到過一個問題康愤,那就是序列化儡循。我們可以通過以下代碼將實例寫入磁盤,然后再從磁盤讀出征冷,即使構(gòu)造方法是私有的择膝,反序列化也是可以通過特殊的途徑去重新創(chuàng)建一個新的實例,代碼如下:

public Singleton createNewInstance() throws IOException, ClassNotFoundException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(baos);
    // 此處的singleton為通過單例模式獲取到的實例對象
    oos.writeObject(singleton);

    ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
    ObjectInputStream ois = new ObjectInputStream(bais);
    // 此時返回一個反序列化后得到的新的實例對象
    return (Singleton) ois.readObject();
}

可以通過上面的代碼看到检激,反序列化后可以得到一個新的實例對象肴捉,那么這種現(xiàn)象沒法避免了嗎腹侣?其實是可以避免的。反序列化提供了一個很特別的方法齿穗,即一個私有傲隶、被實例化的方法readResolve(),這個方法可以讓開發(fā)人員控制對象的反序列化窃页。想要杜絕上面現(xiàn)象的發(fā)生跺株,那么就可以在單例模式中加入readResolve()方法,代碼如下:

private Object readResolve() {
    // 此處返回單例模式中的實例對象
    return sInstance;
}

在《Effective Java》一書中脖卖,作者Joshua Bloch提倡可以采用枚舉的方式來解決上述出現(xiàn)的所有問題乒省,代碼如下:

// 外國老大哥Joshua Bloch的方案
public enum SingletonEnum {
    INSTANCE;
    
    public void method(){
        // do something...
    }
}

可以通過SingletonEnum.INSTANCE獲取單例,然后再調(diào)用內(nèi)部的各種方法畦木。枚舉實現(xiàn)單例有如下好處:

  1. 實例的創(chuàng)建線程安全袖扛,確保單例;
  2. 防止被反射創(chuàng)建多個實例;
  3. 沒有序列化的問題。

雖然這種方法還沒有被廣泛采用馋劈,但是單元素的枚舉類型已經(jīng)成為實現(xiàn)Singleton單例模式的最佳方法攻锰。

3. 總結(jié)

通過上面的一步步分析,不知道大家有沒有對單例模式有個新的認識呢妓雾?總的來說娶吞,加了volatile關(guān)鍵字的雙重校驗鎖和靜態(tài)內(nèi)部類實現(xiàn)的單例模式是目前應(yīng)用最為廣泛的,如果你們要求更嚴的話械姻,那么枚舉單例也不失為一個獲取單例更加的方式妒蛇。歡迎各位能多多交流,指出不足楷拳,共同學習進步绣夺!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市欢揖,隨后出現(xiàn)的幾起案子陶耍,更是在濱河造成了極大的恐慌,老刑警劉巖她混,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件烈钞,死亡現(xiàn)場離奇詭異,居然都是意外死亡坤按,警方通過查閱死者的電腦和手機毯欣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來臭脓,“玉大人酗钞,你說我怎么就攤上這事。” “怎么了砚作?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵窘奏,是天一觀的道長。 經(jīng)常有香客問我葫录,道長蔼夜,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任压昼,我火速辦了婚禮,結(jié)果婚禮上瘤运,老公的妹妹穿的比我還像新娘窍霞。我一直安慰自己,他們只是感情好拯坟,可當我...
    茶點故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布但金。 她就那樣靜靜地躺著,像睡著了一般郁季。 火紅的嫁衣襯著肌膚如雪冷溃。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天梦裂,我揣著相機與錄音似枕,去河邊找鬼。 笑死年柠,一個胖子當著我的面吹牛凿歼,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播冗恨,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼答憔,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了掀抹?” 一聲冷哼從身側(cè)響起虐拓,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎傲武,沒想到半個月后蓉驹,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡谱轨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年戒幔,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片土童。...
    茶點故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡诗茎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情敢订,我是刑警寧澤王污,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站楚午,受9級特大地震影響昭齐,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜矾柜,卻給世界環(huán)境...
    茶點故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一阱驾、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧怪蔑,春花似錦里覆、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至弓坞,卻和暖如春隧甚,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背渡冻。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工戚扳, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人菩帝。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓咖城,卻偏偏與公主長得像,于是被迫代替她去往敵國和親呼奢。 傳聞我的和親對象是個殘疾皇子宜雀,可洞房花燭夜當晚...
    茶點故事閱讀 45,685評論 2 360

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