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

單例模式伏嗜,是特別常見的一種設(shè)計(jì)模式粒蜈,因此我們有必要對(duì)它的概念和幾種常見的寫法非常了解,而且這也是面試中常問的知識(shí)點(diǎn)罐脊。

所謂單例模式定嗓,就是所有的請(qǐng)求都用一個(gè)對(duì)象來處理,如我們常用的Spring默認(rèn)就是單例的爹殊,而多例模式是每一次請(qǐng)求都創(chuàng)建一個(gè)新的對(duì)象來處理蜕乡,如structs2中的action。

使用單例模式梗夸,可以確保一個(gè)類只有一個(gè)實(shí)例层玲,并且易于外部訪問,還可以節(jié)省系統(tǒng)資源反症。如果在系統(tǒng)中辛块,希望某個(gè)類的對(duì)象只存在一個(gè),就可以使用單例模式铅碍。

那怎么確保一個(gè)類只有一個(gè)實(shí)例呢琢融?

我們知道老充,通常我們會(huì)通過new關(guān)鍵字來創(chuàng)建一個(gè)新的對(duì)象女坑。這個(gè)時(shí)候類的構(gòu)造函數(shù)是public公有的,你可以隨意創(chuàng)建多個(gè)類的實(shí)例憨愉。所以,首先我們需要把構(gòu)造函數(shù)改為private私有的卿捎,這樣就不能隨意new對(duì)象了配紫,也就控制了多個(gè)實(shí)例的隨意創(chuàng)建。

然后午阵,定義一個(gè)私有的靜態(tài)屬性躺孝,來代表類的實(shí)例,它只能類內(nèi)部訪問底桂,不允許外部直接訪問植袍。

最后,通過一個(gè)靜態(tài)的公有方法籽懦,把這個(gè)私有靜態(tài)屬性返回出去于个,這就為系統(tǒng)創(chuàng)建了一個(gè)全局唯一的訪問點(diǎn)。

以上猫十,就是單例模式的三個(gè)要素览濒。總結(jié)為:

  1. 私有構(gòu)造方法
  2. 指向自己實(shí)例的私有靜態(tài)變量
  3. 對(duì)外的靜態(tài)公共訪問方法

單例模式分為餓漢式和懶漢式拖云。它們的主要區(qū)別就是,實(shí)例化對(duì)象的時(shí)機(jī)不同应又。餓漢式宙项,是在類加載時(shí)就會(huì)實(shí)例化一個(gè)對(duì)象。懶漢式株扛,則是在真正使用的時(shí)候才會(huì)實(shí)例化對(duì)象尤筐。

餓漢式單例代碼實(shí)現(xiàn):

public class Singleton {

    // 餓漢式單例,直接創(chuàng)建一個(gè)私有的靜態(tài)實(shí)例
    private static Singleton singleton = new Singleton();

    //私有構(gòu)造方法
    private Singleton(){

    }

    //提供一個(gè)對(duì)外的靜態(tài)公有方法
    public static Singleton getInstance(){
        return singleton;

    }
}

懶漢式單例代碼實(shí)現(xiàn)

public class Singleton {

    // 懶漢式單例洞就,類加載時(shí)先不創(chuàng)建實(shí)例
    private static Singleton singleton = null;

    //私有構(gòu)造方法
    private Singleton(){

    }

    //真正使用時(shí)才創(chuàng)建類的實(shí)例
    public static Singleton getInstance(){
        if(singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

稍有經(jīng)驗(yàn)的程序員就發(fā)現(xiàn)了盆繁,以上懶漢式單例的實(shí)現(xiàn)方式,在單線程下是沒有問題的旬蟋。但是油昂,如果在多線程中使用,就會(huì)發(fā)現(xiàn)它們返回的實(shí)例有可能不是同一個(gè)倾贰。我們可以通過代碼來驗(yàn)證一下冕碟。創(chuàng)建十個(gè)線程,分別啟動(dòng)匆浙,線程內(nèi)去獲得類的實(shí)例安寺,把實(shí)例的 hashcode 打印出來,只要相同則認(rèn)為是同一個(gè)實(shí)例首尼;若不同挑庶,則說明創(chuàng)建了多個(gè)實(shí)例言秸。

public class TestSingleton {
    public static void main(String[] args) {
        for (int i = 0; i < 10 ; i++) {
            new MyThread().start();
        }
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        Singleton singleton = Singleton.getInstance();
        System.out.println(singleton.hashCode());
    }
}
/**
運(yùn)行多次,就會(huì)發(fā)現(xiàn)迎捺,hashcode會(huì)出現(xiàn)不同值
668770925
668770925
649030577
668770925
668770925
668770925
668770925
668770925
668770925
668770925
*/

所以井仰,以上懶漢式的實(shí)現(xiàn)方式是線程不安全的。那餓漢式呢破加?你可以手動(dòng)測(cè)試一下俱恶,會(huì)發(fā)現(xiàn)不管運(yùn)行多少次,返回的hashcode都是相同的范舀。因此合是,認(rèn)為餓漢式單例是線程安全的。

那為什么餓漢式就是線程安全的呢锭环?這是因?yàn)榇先I漢式單例在類加載時(shí),就創(chuàng)建了類的實(shí)例辅辩,也就是說在線程去訪問單例對(duì)象之前就已經(jīng)創(chuàng)建好實(shí)例了难礼。而一個(gè)類在整個(gè)生命周期中只會(huì)被加載一次。因此玫锋,也就可以保證實(shí)例只有一個(gè)蛾茉。所以說,餓漢式單例天生就是線程安全的撩鹿。(可以了解一下類加載機(jī)制)

既然懶漢式單例不是線程安全的谦炬,那么我們就需要去改造一下,讓它在多線程環(huán)境下也能正常工作节沦。以下介紹幾種常見的寫法:

  1. 使用synchronized方法

實(shí)現(xiàn)非常簡單键思,只需要在方法上加一個(gè)synchronized關(guān)鍵字即可

public class Singleton {

    private static Singleton singleton = null;

    private Singleton(){

    }

    //使用synchronized修飾方法,即可保證線程安全
    public static synchronized Singleton getInstance(){
        if(singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

這種方式甫贯,雖然可以保證線程安全吼鳞,但是同步方法的作用域太大,鎖的粒度比較粗叫搁,因此赔桌,執(zhí)行效率就比較低。

  1. synchronized 同步塊

既然常熙,同步整個(gè)方法的作用域大纬乍,那我縮小范圍,在方法里邊裸卫,只同步創(chuàng)建實(shí)例的那一小部分代碼塊不就可以了嗎(因?yàn)榉椒ㄝ^簡單仿贬,所以鎖代碼塊和鎖方法沒什么明顯區(qū)別)。

public class Singleton {

    private static Singleton singleton = null;

    private Singleton(){

    }

    public static Singleton getInstance(){
        //synchronized只修飾方法內(nèi)部的部分代碼塊
        synchronized (Singleton.class){
            if(singleton == null){
                singleton = new Singleton();
            }
        }
        return singleton;
    }
}

這種方法墓贿,本質(zhì)上和第一種沒什么區(qū)別茧泪,因此蜓氨,效率提升不大,可以忽略不計(jì)队伟。

  1. 雙重檢測(cè)(double check)

可以看到穴吹,以上的第二種方法只要調(diào)用getInstance方法,就會(huì)走到同步代碼塊里嗜侮。因此港令,會(huì)對(duì)效率產(chǎn)生影響。其實(shí)锈颗,我們完全可以先判斷實(shí)例是否已經(jīng)存在顷霹。若已經(jīng)存在,則說明已經(jīng)創(chuàng)建好實(shí)例了击吱,也就不需要走同步代碼塊了淋淀;若不存在即為空,才進(jìn)入同步代碼塊覆醇,這樣可以提高執(zhí)行效率朵纷。因此,就有以下雙重檢測(cè)了:

public class Singleton {

    //注意永脓,此變量需要用volatile修飾以防止指令重排序
    private static volatile Singleton singleton = null;

    private Singleton(){

    }

    public static Singleton getInstance(){
        //進(jìn)入方法內(nèi)袍辞,先判斷實(shí)例是否為空,以確定是否需要進(jìn)入同步代碼塊
        if(singleton == null){
            synchronized (Singleton.class){
                //進(jìn)入同步代碼塊時(shí)也需要判斷實(shí)例是否為空
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

需要注意的一點(diǎn)是憨奸,此方式中革屠,靜態(tài)實(shí)例變量需要用volatile修飾。因?yàn)榕旁祝琻ew Singleton() 是一個(gè)非原子性操作,其流程為:

a.給 singleton 實(shí)例分配內(nèi)存空間
b.調(diào)用Singleton類的構(gòu)造函數(shù)創(chuàng)建實(shí)例
c.將 singleton 實(shí)例指向分配的內(nèi)存空間那婉,這時(shí)認(rèn)為singleton實(shí)例不為空

正常順序?yàn)?a->b->c板甘,但是,jvm為了優(yōu)化編譯程序详炬,有時(shí)候會(huì)進(jìn)行指令重排序盐类。就會(huì)出現(xiàn)執(zhí)行順序?yàn)?a->c->b。這在多線程中就會(huì)表現(xiàn)為呛谜,線程1執(zhí)行了new對(duì)象操作在跳,然后發(fā)生了指令重排序,會(huì)導(dǎo)致singleton實(shí)例已經(jīng)指向了分配的內(nèi)存空間(c)隐岛,但是實(shí)際上猫妙,實(shí)例還沒創(chuàng)建完成呢(b)。

這個(gè)時(shí)候聚凹,線程2就會(huì)認(rèn)為實(shí)例不為空割坠,判斷 if(singleton == null)為false齐帚,于是不走同步代碼塊,直接返回singleton實(shí)例(此時(shí)拿到的是未實(shí)例化的對(duì)象)彼哼,因此对妄,就會(huì)導(dǎo)致線程2的對(duì)象不可用而使用時(shí)報(bào)錯(cuò)。

4)使用靜態(tài)內(nèi)部類

思考一下敢朱,由于類加載是按需加載剪菱,并且只加載一次,所以能保證線程安全拴签,這也是為什么說餓漢式單例是天生線程安全的孝常。同樣的道理,我們是不是也可以通過定義一個(gè)靜態(tài)內(nèi)部類來保證類屬性只被加載一次呢篓吁。

public class Singleton {

    private Singleton(){

    }

    //靜態(tài)內(nèi)部類
    private static class Holder {
        private static Singleton singleton = new Singleton();
    }

    public static Singleton getInstance(){
        //調(diào)用內(nèi)部類的屬性茫因,獲取單例對(duì)象
        return Holder.singleton;
    }
}

而且,JVM在加載外部類的時(shí)候杖剪,不會(huì)加載靜態(tài)內(nèi)部類冻押,只有在內(nèi)部類的方法或?qū)傩裕ù颂幖粗竤ingleton實(shí)例)被調(diào)用時(shí)才會(huì)加載,因此不會(huì)造成空間的浪費(fèi)盛嘿。

5)使用枚舉類

因?yàn)槊杜e類是線程安全的洛巢,并且只會(huì)加載一次,所以利用這個(gè)特性次兆,可以通過枚舉類來實(shí)現(xiàn)單例稿茉。

public class Singleton {

    private Singleton(){

    }

    //定義一個(gè)枚舉類
    private enum SingletonEnum {
        //創(chuàng)建一個(gè)枚舉實(shí)例
        INSTANCE;

        private Singleton singleton;

        //在枚舉類的構(gòu)造方法內(nèi)實(shí)例化單例類
        SingletonEnum(){
            singleton = new Singleton();
        }

        private Singleton getInstance(){
            return singleton;
        }
    }

    public static Singleton getInstance(){
        //獲取singleton實(shí)例
        return SingletonEnum.INSTANCE.getInstance();
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市芥炭,隨后出現(xiàn)的幾起案子漓库,更是在濱河造成了極大的恐慌,老刑警劉巖园蝠,帶你破解...
    沈念sama閱讀 216,843評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件渺蒿,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡彪薛,警方通過查閱死者的電腦和手機(jī)茂装,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,538評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來善延,“玉大人少态,你說我怎么就攤上這事∫浊玻” “怎么了彼妻?”我有些...
    開封第一講書人閱讀 163,187評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長训挡。 經(jīng)常有香客問我澳骤,道長歧强,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,264評(píng)論 1 292
  • 正文 為了忘掉前任为肮,我火速辦了婚禮摊册,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘颊艳。我一直安慰自己茅特,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,289評(píng)論 6 390
  • 文/花漫 我一把揭開白布棋枕。 她就那樣靜靜地躺著白修,像睡著了一般。 火紅的嫁衣襯著肌膚如雪重斑。 梳的紋絲不亂的頭發(fā)上兵睛,一...
    開封第一講書人閱讀 51,231評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音窥浪,去河邊找鬼祖很。 笑死,一個(gè)胖子當(dāng)著我的面吹牛漾脂,可吹牛的內(nèi)容都是我干的假颇。 我是一名探鬼主播,決...
    沈念sama閱讀 40,116評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼骨稿,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼笨鸡!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起坦冠,我...
    開封第一講書人閱讀 38,945評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤形耗,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后辙浑,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體趟脂,經(jīng)...
    沈念sama閱讀 45,367評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,581評(píng)論 2 333
  • 正文 我和宋清朗相戀三年例衍,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片已卸。...
    茶點(diǎn)故事閱讀 39,754評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡佛玄,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出累澡,到底是詐尸還是另有隱情梦抢,我是刑警寧澤,帶...
    沈念sama閱讀 35,458評(píng)論 5 344
  • 正文 年R本政府宣布愧哟,位于F島的核電站奥吩,受9級(jí)特大地震影響哼蛆,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜霞赫,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,068評(píng)論 3 327
  • 文/蒙蒙 一腮介、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧端衰,春花似錦叠洗、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,692評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至抵代,卻和暖如春腾节,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背荤牍。 一陣腳步聲響...
    開封第一講書人閱讀 32,842評(píng)論 1 269
  • 我被黑心中介騙來泰國打工案腺, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人参淫。 一個(gè)月前我還...
    沈念sama閱讀 47,797評(píng)論 2 369
  • 正文 我出身青樓救湖,卻偏偏與公主長得像,于是被迫代替她去往敵國和親涎才。 傳聞我的和親對(duì)象是個(gè)殘疾皇子鞋既,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,654評(píng)論 2 354

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