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

1. 什么是單例模式

單例模式指的是在應(yīng)用整個(gè)生命周期內(nèi)只能存在一個(gè)實(shí)例媒至。單例模式是一種被廣泛使用的設(shè)計(jì)模式今膊。他有很多好處,能夠避免實(shí)例對象的重復(fù)創(chuàng)建狐粱,減少創(chuàng)建實(shí)例的系統(tǒng)開銷舀寓,節(jié)省內(nèi)存。

2. 單例模式和靜態(tài)類的區(qū)別

首先理解一下什么是靜態(tài)類肌蜻,靜態(tài)類就是一個(gè)類里面都是靜態(tài)方法和靜態(tài)field互墓,構(gòu)造器被private修飾,因此不能被實(shí)例化蒋搜。Math類就是一個(gè)靜態(tài)類篡撵。

知道了什么是靜態(tài)類后,來說一下他們兩者之間的區(qū)別:

  • 首先單例模式會提供給你一個(gè)全局唯一的對象豆挽,靜態(tài)類只是提供給你很多靜態(tài)方法育谬,這些方法不用創(chuàng)建對象,通過類就可以直接調(diào)用祷杈;
  • 單例模式的靈活性更高斑司,方法可以被override,因?yàn)殪o態(tài)類都是靜態(tài)方法但汞,所以不能被override;
  • 如果是一個(gè)非常重的對象互站,單例模式可以懶加載私蕾,靜態(tài)類就無法做到;

那么時(shí)候時(shí)候應(yīng)該用靜態(tài)類胡桃,什么時(shí)候應(yīng)該用單例模式呢踩叭?首先如果你只是想使用一些工具方法,那么最好用靜態(tài)類翠胰,靜態(tài)類比單例類更快容贝,因?yàn)殪o態(tài)的綁定是在編譯期進(jìn)行的。如果你要維護(hù)狀態(tài)信息之景,或者訪問資源時(shí)斤富,應(yīng)該選用單例模式。還可以這樣說锻狗,當(dāng)你需要面向?qū)ο蟮哪芰r(shí)(比如繼承满力、多態(tài))時(shí)焕参,選用單例類,當(dāng)你僅僅是提供一些方法時(shí)選用靜態(tài)類油额。

3.如何實(shí)現(xiàn)單例模式

1. 餓漢模式

所謂餓漢模式就是立即加載叠纷,一般情況下再調(diào)用getInstancef方法之前就已經(jīng)產(chǎn)生了實(shí)例,也就是在類加載的時(shí)候已經(jīng)產(chǎn)生了潦嘶。這種模式的缺點(diǎn)很明顯涩嚣,就是占用資源,當(dāng)單例類很大的時(shí)候掂僵,其實(shí)我們是想使用的時(shí)候再產(chǎn)生實(shí)例缓艳。因此這種方式適合占用資源少,在初始化的時(shí)候就會被用到的類看峻。

class SingletonHungary {
    private static SingletonHungary singletonHungary = new SingletonHungary();
    //將構(gòu)造器設(shè)置為private禁止通過new進(jìn)行實(shí)例化
    private SingletonHungary() {

    }
    public static SingletonHungary getInstance() {
        return singletonHungary;
    }
}

2. 懶漢模式

懶漢模式就是延遲加載阶淘,也叫懶加載。在程序需要用到的時(shí)候再創(chuàng)建實(shí)例互妓,這樣保證了內(nèi)存不會被浪費(fèi)溪窒。針對懶漢模式,這里給出了5種實(shí)現(xiàn)方式冯勉,有些實(shí)現(xiàn)方式是線程不安全的澈蚌,也就是說在多線程并發(fā)的環(huán)境下可能出現(xiàn)資源同步問題。

首先第一種方式灼狰,在單線程下沒問題宛瞄,在多線程下就出現(xiàn)問題了。

// 單例模式的懶漢實(shí)現(xiàn)1--線程不安全
class SingletonLazy1 {
    private static SingletonLazy1 singletonLazy;

    private SingletonLazy1() {

    }

    public static SingletonLazy1 getInstance() {
        if (null == singletonLazy) {
            try {
                // 模擬在創(chuàng)建對象之前做一些準(zhǔn)備工作
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            singletonLazy = new SingletonLazy1();
        }
        return singletonLazy;
    }
}

我們模擬10個(gè)異步線程測試一下:

public class SingletonLazyTest {

    public static void main(String[] args) {

        Thread2[] ThreadArr = new Thread2[10];
        for (int i = 0; i < ThreadArr.length; i++) {
            ThreadArr[i] = new Thread2();
            ThreadArr[i].start();
        }
    }

}

// 測試線程
class Thread2 extends Thread {
    @Override
    public void run() {
        System.out.println(SingletonLazy1.getInstance().hashCode());
    }
}

運(yùn)行結(jié)果:
124191239
124191239
872096466
1603289047
1698032342
1913667618
371739364
124191239
1723650563
36713730312345678910

可以看到他們的hashCode不都是一樣的交胚,說明在多線程環(huán)境下份汗,產(chǎn)生了多個(gè)對象,不符合單例模式的要求蝴簇。

那么如何使線程安全呢杯活?第二種方法,我們使用synchronized關(guān)鍵字對getInstance方法進(jìn)行同步熬词。

// 單例模式的懶漢實(shí)現(xiàn)2--線程安全
// 通過設(shè)置同步方法旁钧,效率太低,整個(gè)方法被加鎖
class SingletonLazy2 {
    private static SingletonLazy2 singletonLazy;

    private SingletonLazy2() {

    }

    public static synchronized SingletonLazy2 getInstance() {
        try {
            if (null == singletonLazy) {
                // 模擬在創(chuàng)建對象之前做一些準(zhǔn)備工作
                Thread.sleep(1000);
                singletonLazy = new SingletonLazy2();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return singletonLazy;
    }
}

使用上面的測試類互拾,測試結(jié)果:

1210004989
1210004989
1210004989
1210004989
1210004989
1210004989
1210004989
1210004989
1210004989
121000498912345678910

可以看到歪今,這種方式達(dá)到了線程安全。但是缺點(diǎn)就是效率太低颜矿,是同步運(yùn)行的寄猩,下個(gè)線程想要取得對象,就必須要等上一個(gè)線程釋放或衡,才可以繼續(xù)執(zhí)行焦影。

那我們可以不對方法加鎖车遂,而是將里面的代碼加鎖,也可以實(shí)現(xiàn)線程安全斯辰。但這種方式和同步方法一樣舶担,也是同步運(yùn)行的,效率也很低彬呻。

// 單例模式的懶漢實(shí)現(xiàn)3--線程安全
// 通過設(shè)置同步代碼塊衣陶,效率也太低,整個(gè)代碼塊被加鎖
class SingletonLazy3 {

    private static SingletonLazy3 singletonLazy;

    private SingletonLazy3() {

    }

    public static SingletonLazy3 getInstance() {
        try {
            synchronized (SingletonLazy3.class) {
                if (null == singletonLazy) {
                    // 模擬在創(chuàng)建對象之前做一些準(zhǔn)備工作
                    Thread.sleep(1000);
                    singletonLazy = new SingletonLazy3();
                }
            }
        } catch (InterruptedException e) {
            // TODO: handle exception
        }
        return singletonLazy;
    }
}

我們來繼續(xù)優(yōu)化代碼闸氮,我們只給創(chuàng)建對象的代碼進(jìn)行加鎖剪况,但是這樣能保證線程安全么?

// 單例模式的懶漢實(shí)現(xiàn)4--線程不安全
// 通過設(shè)置同步代碼塊蒲跨,只同步創(chuàng)建實(shí)例的代碼
// 但是還是有線程安全問題
class SingletonLazy4 {

    private static SingletonLazy4 singletonLazy;

    private SingletonLazy4() {

    }

    public static SingletonLazy4 getInstance() {
        try {
            if (null == singletonLazy) {        //代碼1
                // 模擬在創(chuàng)建對象之前做一些準(zhǔn)備工作
                Thread.sleep(1000);
                synchronized (SingletonLazy4.class) {
                    singletonLazy = new SingletonLazy4(); //代碼2
                }
            }
        } catch (InterruptedException e) {
            // TODO: handle exception
        }
        return singletonLazy;
    }
}

我們來看一下運(yùn)行結(jié)果:
1210004989
1425839054
1723650563
389001266
1356914048
389001266
1560241484
278778395
124191239
36713730312345678910

從結(jié)果看來译断,這種方式不能保證線程安全,為什么呢或悲?我們假設(shè)有兩個(gè)線程A和B同時(shí)走到了‘代碼1’孙咪,因?yàn)榇藭r(shí)對象還是空的,所以都能進(jìn)到方法里面巡语,線程A首先搶到鎖翎蹈,創(chuàng)建了對象。釋放鎖后線程B拿到了鎖也會走到‘代碼2’男公,也創(chuàng)建了一個(gè)對象荤堪,因此多線程環(huán)境下就不能保證單例了。

讓我們來繼續(xù)優(yōu)化一下枢赔,既然上述方式存在問題澄阳,那我們在同步代碼塊里面再一次做一下null判斷不就行了,這種方式就是我們的DCL雙重檢查鎖機(jī)制糠爬。

//單例模式的懶漢實(shí)現(xiàn)5--線程安全
//通過設(shè)置同步代碼塊寇荧,使用DCL雙檢查鎖機(jī)制
//使用雙檢查鎖機(jī)制成功的解決了單例模式的懶漢實(shí)現(xiàn)的線程不安全問題和效率問題
//DCL 也是大多數(shù)多線程結(jié)合單例模式使用的解決方案
class SingletonLazy5 {

    private static SingletonLazy5 singletonLazy;

    private SingletonLazy5() {

    }

    public static SingletonLazy5 getInstance() {
        try {
            if (null == singletonLazy) {
                // 模擬在創(chuàng)建對象之前做一些準(zhǔn)備工作
                Thread.sleep(1000);
                synchronized (SingletonLazy5.class) {
                    if(null == singletonLazy) {
                        singletonLazy = new SingletonLazy5();
                    }
                }
            }
        } catch (InterruptedException e) {
            // TODO: handle exception
        }
        return singletonLazy;
    }
}

運(yùn)行結(jié)果:

124191239
124191239
124191239
124191239
124191239
124191239
124191239
124191239
124191239
12419123912345678910

我們可以看到DCL雙重檢查鎖機(jī)制很好的解決了懶加載單例模式的效率問題和線程安全問題。這也是我們最常用到的方式执隧。

3. 靜態(tài)內(nèi)部類

我們也可以使用靜態(tài)內(nèi)部類實(shí)現(xiàn)單例模式,代碼如下:

//使用靜態(tài)內(nèi)部類實(shí)現(xiàn)單例模式--線程安全
class SingletonStaticInner {
    private SingletonStaticInner() {

    }
    private static class SingletonInner {
        private static SingletonStaticInner singletonStaticInner = new SingletonStaticInner();
    }
    public static SingletonStaticInner getInstance() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return SingletonInner.singletonStaticInner;
    }
}

可以看到使用這種方式我們沒有顯式的進(jìn)行任何同步操作户侥,那他是如何保證線程安全呢镀琉?和餓漢模式一樣,是靠JVM保證類的靜態(tài)成員只能被加載一次的特點(diǎn)蕊唐,這樣就從JVM層面保證了只會有一個(gè)實(shí)例對象屋摔。那么問題來了,這種方式和餓漢模式又有什么區(qū)別呢替梨?不也是立即加載么钓试?實(shí)則不然装黑,加載一個(gè)類時(shí),其內(nèi)部類不會同時(shí)被加載弓熏。一個(gè)類被加載恋谭,當(dāng)且僅當(dāng)其某個(gè)靜態(tài)成員(靜態(tài)域、構(gòu)造器挽鞠、靜態(tài)方法等)被調(diào)用時(shí)發(fā)生疚颊。

可以說這種方式是實(shí)現(xiàn)單例模式的最優(yōu)解。

4. 靜態(tài)代碼塊

這里提供了靜態(tài)代碼塊實(shí)現(xiàn)單例模式信认。這種方式和第一種類似材义,也是一種餓漢模式。


//使用靜態(tài)代碼塊實(shí)現(xiàn)單例模式
class SingletonStaticBlock {
    private static SingletonStaticBlock singletonStaticBlock;
    static {
        singletonStaticBlock = new SingletonStaticBlock();
    }
    public static SingletonStaticBlock getInstance() {
        return singletonStaticBlock;
    }
}

5. 序列化與反序列化

LZ為什么要提序列化和反序列化呢嫁赏?因?yàn)閱卫J诫m然能保證線程安全其掂,但在序列化和反序列化的情況下會出現(xiàn)生成多個(gè)對象的情況。運(yùn)行下面的測試類潦蝇,

public class SingletonStaticInnerSerializeTest {

    public static void main(String[] args) {
        try {
            SingletonStaticInnerSerialize serialize = SingletonStaticInnerSerialize.getInstance();
            System.out.println(serialize.hashCode());
            //序列化
            FileOutputStream fo = new FileOutputStream("tem");
            ObjectOutputStream oo = new ObjectOutputStream(fo);
            oo.writeObject(serialize);
            oo.close();
            fo.close();
            //反序列化
            FileInputStream fi = new FileInputStream("tem");
            ObjectInputStream oi = new ObjectInputStream(fi);
            SingletonStaticInnerSerialize serialize2 = (SingletonStaticInnerSerialize) oi.readObject();
            oi.close();
            fi.close();
            System.out.println(serialize2.hashCode());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

//使用匿名內(nèi)部類實(shí)現(xiàn)單例模式款熬,在遇見序列化和反序列化的場景朵逝,得到的不是同一個(gè)實(shí)例
//解決這個(gè)問題是在序列化的時(shí)候使用readResolve方法硕旗,即去掉注釋的部分
class SingletonStaticInnerSerialize implements Serializable {

    /**
     * 2018年03月28日
     */
    private static final long serialVersionUID = 1L;

    private static class InnerClass {
        private static SingletonStaticInnerSerialize singletonStaticInnerSerialize = new SingletonStaticInnerSerialize();
    }

    public static SingletonStaticInnerSerialize getInstance() {
        return InnerClass.singletonStaticInnerSerialize;
    }

//  protected Object readResolve() {
//      System.out.println("調(diào)用了readResolve方法");
//      return InnerClass.singletonStaticInnerSerialize;
//  }
}

可以看到:

865113938
107869478912

結(jié)果表明的確是兩個(gè)不同的對象實(shí)例,違背了單例模式酿矢,那么如何解決這個(gè)問題呢持灰?解決辦法就是在反序列化中使用readResolve()方法盔夜,將上面的注釋代碼去掉,再次運(yùn)行:

865113938
調(diào)用了readResolve方法
865113938123

問題來了堤魁,readResolve()方法到底是何方神圣喂链,其實(shí)當(dāng)JVM從內(nèi)存中反序列化地”組裝”一個(gè)新對象時(shí),就會自動(dòng)調(diào)用這個(gè) readResolve方法來返回我們指定好的對象了, 單例規(guī)則也就得到了保證妥泉。readResolve()的出現(xiàn)允許程序員自行控制通過反序列化得到的對象椭微。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市盲链,隨后出現(xiàn)的幾起案子蝇率,更是在濱河造成了極大的恐慌,老刑警劉巖刽沾,帶你破解...
    沈念sama閱讀 223,002評論 6 519
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件本慕,死亡現(xiàn)場離奇詭異,居然都是意外死亡侧漓,警方通過查閱死者的電腦和手機(jī)锅尘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,357評論 3 400
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來布蔗,“玉大人藤违,你說我怎么就攤上這事浪腐。” “怎么了顿乒?”我有些...
    開封第一講書人閱讀 169,787評論 0 365
  • 文/不壞的土叔 我叫張陵议街,是天一觀的道長。 經(jīng)常有香客問我淆游,道長傍睹,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,237評論 1 300
  • 正文 為了忘掉前任犹菱,我火速辦了婚禮拾稳,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘腊脱。我一直安慰自己访得,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,237評論 6 398
  • 文/花漫 我一把揭開白布陕凹。 她就那樣靜靜地躺著悍抑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪杜耙。 梳的紋絲不亂的頭發(fā)上搜骡,一...
    開封第一講書人閱讀 52,821評論 1 314
  • 那天,我揣著相機(jī)與錄音佑女,去河邊找鬼记靡。 笑死,一個(gè)胖子當(dāng)著我的面吹牛团驱,可吹牛的內(nèi)容都是我干的摸吠。 我是一名探鬼主播,決...
    沈念sama閱讀 41,236評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼嚎花,長吁一口氣:“原來是場噩夢啊……” “哼寸痢!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起紊选,我...
    開封第一講書人閱讀 40,196評論 0 277
  • 序言:老撾萬榮一對情侶失蹤啼止,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后兵罢,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體族壳,經(jīng)...
    沈念sama閱讀 46,716評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,794評論 3 343
  • 正文 我和宋清朗相戀三年趣些,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片贰您。...
    茶點(diǎn)故事閱讀 40,928評論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡坏平,死狀恐怖拢操,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情舶替,我是刑警寧澤令境,帶...
    沈念sama閱讀 36,583評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站顾瞪,受9級特大地震影響舔庶,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜陈醒,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,264評論 3 336
  • 文/蒙蒙 一惕橙、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧钉跷,春花似錦弥鹦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,755評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至膝晾,卻和暖如春栓始,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背血当。 一陣腳步聲響...
    開封第一講書人閱讀 33,869評論 1 274
  • 我被黑心中介騙來泰國打工幻赚, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人歹颓。 一個(gè)月前我還...
    沈念sama閱讀 49,378評論 3 379
  • 正文 我出身青樓坯屿,卻偏偏與公主長得像,于是被迫代替她去往敵國和親巍扛。 傳聞我的和親對象是個(gè)殘疾皇子领跛,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,937評論 2 361

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