淺析Java并發(fā)中的單例模式

一、單例模式簡(jiǎn)介

單例模式,是一種常用的軟件設(shè)計(jì)模式别渔。在它的核心結(jié)構(gòu)中只包含一個(gè)被稱為單例的特殊類。通過(guò)單例模式可以保證系統(tǒng)中惧互,應(yīng)用該模式的類一個(gè)類只有一個(gè)實(shí)例哎媚。即一個(gè)類只有一個(gè)對(duì)象實(shí)例。在java代碼中喊儡,通常new關(guān)鍵字創(chuàng)造出來(lái)的對(duì)象拨与,對(duì)系統(tǒng)的開(kāi)銷一般都挺大的。所以在某些情況下艾猜,單例的實(shí)現(xiàn)也是應(yīng)對(duì)系統(tǒng)優(yōu)化的一種解決辦法买喧。

二、單例模式的實(shí)現(xiàn)

常見(jiàn)的單例有這幾種實(shí)現(xiàn)

  • 餓漢式
  • 飽漢式
  • 雙重校驗(yàn)
  • 靜態(tài)內(nèi)部類

1匆赃、餓漢式

先來(lái)介紹餓漢式淤毛,餓漢式,顧名思義算柳,就是一進(jìn)入這個(gè)類低淡,該類的實(shí)例就被初始化完成了。接下來(lái)來(lái)看下代碼。

public class Demo {
    private static Demo h = new Demo();
    private Demo(){
    }
    public static Demo getInstance(){
        return h;
    }
}

代碼也和簡(jiǎn)單查牌,就是直接構(gòu)造一個(gè)私有的構(gòu)造器事期,然后在建立一個(gè)成員變量,順便實(shí)例化該類纸颜,在調(diào)用該類的getInstance方法,當(dāng)然前面也說(shuō)過(guò)是一進(jìn)入這個(gè)類绎橘,

該類的實(shí)例就被創(chuàng)建完成胁孙,所以也可以利用類的加載順序來(lái)編寫這個(gè)代碼。比如 A類是B類的子類称鳞,在初始化A類的實(shí)例的時(shí)候涮较,會(huì)先去父類B中去,看看有沒(méi)有靜態(tài)塊和靜態(tài)成員變量(靜態(tài)方法只有被調(diào)用時(shí)才會(huì)加載冈止,且只會(huì)被加載一次)狂票,若有則先去加載B類的靜態(tài)塊和靜態(tài)成員變量,再加載A類的熙暴。之后會(huì)去調(diào)用B的構(gòu)造器闺属,最后才會(huì)調(diào)用本類對(duì)應(yīng)的構(gòu)造器。

所以我們可以在靜態(tài)代碼塊中實(shí)例化周霉。如下代碼

public class Demo {
    private static Demo h = null;
    private Demo(){
    }
    static{
        h = new Demo();
    }
    public static Demo getInstance(){
        return h;
    }
}

該種實(shí)現(xiàn)的單例是線程安全的掂器。當(dāng)然由于它會(huì)提前初始化,所以會(huì)提前占用一些系統(tǒng)資源俱箱。

2国瓮、飽漢式

飽漢式的構(gòu)造實(shí)例的時(shí)候與餓汗式相反,它只有在第一次需要的時(shí)候才會(huì)去構(gòu)造實(shí)例狞谱。具體實(shí)現(xiàn)代碼如下

public class Demo {
    private static Demo h = null;
    private Demo(){
    }
    public static Demo getInstance(){
        if(h == null){
            //1
            h = new Demo();
            //2
        }
        return h;
    }
}

飽漢式最常見(jiàn)的的編寫方式就是上述代碼乃摹,對(duì)于剛了解單例模式的人來(lái)說(shuō),飽漢式就寫完了跟衅,不過(guò)在單線程環(huán)境也確實(shí)可以說(shuō)是寫完了孵睬,A線程在獲取實(shí)例,第一次獲取時(shí)与斤,看見(jiàn)為null肪康,進(jìn)行初始化,第二次撩穿,不是null磷支,直接返回。這也是一種很理想的流程食寡。但是值得注意的是雾狈,在多線程下,它就值得推敲了抵皱。比如看下面例子

  • 線程A:嘿嘿善榛!我已經(jīng)走到了2辩蛋,這初始化的好處我就要獨(dú)占了,想想都雞凍移盆,我要去初始化Demo類的實(shí)例了悼院,啦啦啦。
  • 線程B:哈哈咒循!線程A那個(gè)SD据途,我都走到了1,它竟然還沒(méi)發(fā)現(xiàn)我叙甸,還想獨(dú)占Demo的實(shí)例化颖医,沒(méi)門!裆蒸!

旁白:線程A還完成了對(duì)該類實(shí)例的初始化熔萧,線程B也進(jìn)入了對(duì)該實(shí)例的構(gòu)造中,

因此僚祷,線程A和線程B都同時(shí)初始化了該實(shí)例佛致,這也不滿足單例的條件。

于是有人很自然的想到久妆,加鎖晌杰。如下

public class Demo {
    private static Demo h = null;
    private Demo(){
    }
    public synchronized static Demo getInstance(){
        if(h == null){
            h = new Demo();
        }
        return h;
    }
}

這樣確實(shí)可以防止多線程環(huán)境造成多個(gè)實(shí)例。但的缺點(diǎn)是每一次獲取都去加鎖筷弦,會(huì)對(duì)性能有一定的損失肋演。所以有了雙重校驗(yàn)鎖。

3烂琴、雙重校驗(yàn)

雙重校驗(yàn)爹殊,就是在獲取單例的時(shí)候,對(duì)加鎖的方式進(jìn)行了改變奸绷,它不在方法上加鎖梗夸,它對(duì)代碼塊進(jìn)行加鎖,這樣的效率比飽漢式要高号醉。具體代碼如下

public class Demo {
    private static Demo h = null;
    private Demo(){
    }
    public static Demo getInstance(){
        if(h == null){
            //1
            synchronized (new Object()) {
                //2
                if(h == null){
                    h = new Demo();
                }
            }
        }
        return h;
    }
}

也許有人看了以上代碼后會(huì)有疑問(wèn)反症,要加上兩個(gè)if判斷干嘛?加一個(gè)不行嗎畔派,比如如下

public class Demo {
    private static Demo h = null;
    private Demo(){
    }
    public static Demo getInstance(){
        if(h == null){
            //1
            synchronized (new Object()) {
                //2
                h = new Demo();
            }
        }
        return h;
    }
}

這樣不也對(duì)進(jìn)行實(shí)例化的時(shí)候加鎖了嗎铅碍?也可以保證線程安全啊线椰!

看這個(gè)例子

  • 線程A:嘿嘿胞谈!我已經(jīng)走到了2,咦!竟然還有鎖烦绳,更好了卿捎,這初始化的好處我就要獨(dú)占了,想想都雞凍径密,我要去初始化Demo類的實(shí)例了午阵,啦啦啦。
  • 線程B:哈哈睹晒!線程A那個(gè)SD趟庄,我都走到了1,它竟然還沒(méi)發(fā)現(xiàn)我伪很,還想獨(dú)占Demo的實(shí)例化,沒(méi)門7艿ァ锉试!臥槽,靜態(tài)被線程A那個(gè)SD上鎖了览濒,哎等等吧呆盖!
  • 線程B:咦!鎖的鑰匙又回來(lái)了贷笛,哎应又,沒(méi)希望了,希望能給我喝點(diǎn)湯乏苦。n秒后株扛,B處于驚訝中,沒(méi)想到我還能初始化汇荐。哈哈哈洞就。

為啥雙重會(huì)被認(rèn)為是線程安全的∠铺裕看這個(gè)例子

  • 線程A:嘿嘿旬蟋!我已經(jīng)走到了2,咦革娄!竟然還有鎖倾贰,更好了,這初始化的好處我就要獨(dú)占了拦惋,想想都雞凍匆浙,我要去初始化Demo類的實(shí)例了,啦啦啦架忌。
  • 線程B:哈哈吞彤!線程A那個(gè)SD,我都走到了1,它竟然還沒(méi)發(fā)現(xiàn)我饰恕,還想獨(dú)占Demo的實(shí)例化挠羔,沒(méi)門!埋嵌!臥槽破加,靜態(tài)被線程A那個(gè)SD上鎖了,哎等等吧雹嗦!
  • 線程B:咦范舀!鎖的鑰匙又回來(lái)了,哎了罪,沒(méi)希望了锭环,希望能給我喝點(diǎn)湯。n秒后泊藕,B處于崩潰中辅辩,沒(méi)想到竟然還有if(h == null)這個(gè)大門,我進(jìn)不去了娃圆,嗚嗚嗚玫锋。

值得注意的是,該種產(chǎn)生單例的方式也會(huì)有線程安全的問(wèn)題讼呢,學(xué)過(guò)java的都知道撩鹿,java中在new對(duì)象的時(shí)候,并不是原子操作悦屏,它有以下三個(gè)大概步驟

  • 分配內(nèi)存空間
  • 初始化對(duì)象
  • 將內(nèi)存地址賦給引用h

由于重排序原因(關(guān)于重排序的知識(shí)节沦,可自行網(wǎng)上搜索),可能在new對(duì)象時(shí)窜管,第二步和第三步發(fā)生了交換散劫,導(dǎo)致錯(cuò)誤發(fā)生,理由是幕帆,此時(shí)的h是一個(gè)地址获搏,但它

還沒(méi)完成初始化,如下例子

  • 線程A:嘿嘿失乾!我已經(jīng)走到了2常熙,咦!竟然還有鎖碱茁,更好了裸卫,這初始化的好處我就要獨(dú)占了,想想都雞凍纽竣,我要去初始化Demo類的實(shí)例了墓贿,啦啦啦茧泪。
  • 線程B:哈哈!線程A那個(gè)SD聋袋,我都走到了1队伟,它竟然還沒(méi)發(fā)現(xiàn)我,還想獨(dú)占Demo的實(shí)例化幽勒,沒(méi)門J任辍!臥槽啥容,靜態(tài)被線程A那個(gè)SD上鎖了锈颗,哎等等吧!
  • 線程B:咦咪惠!鎖的鑰匙又回來(lái)了击吱,哎,沒(méi)希望了遥昧,希望能給我喝點(diǎn)湯姨拥。n秒后,B處于崩潰中渠鸽,沒(méi)想到竟然還有if(h == null)這個(gè)大門。
  • 線程B:只能認(rèn)命了柴罐,我只能訪問(wèn)Demo對(duì)象玩玩徽缚,噗噗噗,竟然出錯(cuò)了革屠。凿试。

其原因就如上述所說(shuō),發(fā)生了重排序?qū)е碌腻e(cuò)誤發(fā)生似芝,當(dāng)然那婉,這個(gè)錯(cuò)誤不一定經(jīng)常發(fā)生。所有這時(shí)應(yīng)該想的是怎么防止重排序党瓮。

于是有人就想到了java中的volatile關(guān)鍵字來(lái)禁止代碼的重排序详炬,當(dāng)然它還有保證可見(jiàn)性的功能,但不能和synchronized一樣還能保證原子性寞奸。

加了volatile關(guān)鍵字后呛谜,這樣才算真的線程安全,具體代碼如下

public class Demo {
    private volatile static Demo h = null;
    private Demo(){
    }
    public static Demo getInstance(){
        if(h == null){
            synchronized (new Object()) {
                h = new Demo();
            }
        }
        return h;
    }
}

4颂砸、靜態(tài)內(nèi)部類

靜態(tài)內(nèi)部類就是在該類的內(nèi)部實(shí)現(xiàn)一個(gè)靜態(tài)內(nèi)部類蠢壹,內(nèi)部類里來(lái)實(shí)現(xiàn)該類的實(shí)例化牵舱,具體代碼如下

public class Demo {
    private volatile static Demo h = null;
    private Demo(){
    }
    public static Demo getInstance(){
        return InnerDemo.h;
    }
    //內(nèi)部類
    private static class InnerDemo{
        private final static Demo h = new Demo();
    }
}

內(nèi)部類的原理是利用了類加載器classloader機(jī)制來(lái)保證初始化h時(shí)只有一個(gè)線程,這樣也就保證了線程的安全性 聚凹。同時(shí)也不像餓汗式一樣割坠,一進(jìn)入該類就觸發(fā)實(shí)例初始化。內(nèi)部類雖然是static妒牙,但只有在return InnerDemo.h時(shí)才會(huì)觸發(fā)該內(nèi)部類的加載彼哼,也是懶加載的一種。

讀者福利:

分享免費(fèi)學(xué)習(xí)資料

針對(duì)于Java程序員单旁,我這邊準(zhǔn)備免費(fèi)的Java架構(gòu)學(xué)習(xí)資料(里面有高可用沪羔、高并發(fā)、高性能及分布式象浑、Jvm性能調(diào)優(yōu)蔫饰、MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個(gè)知識(shí)點(diǎn)的架構(gòu)資料)

為什么某些人會(huì)一直比你優(yōu)秀愉豺,是因?yàn)樗旧砭秃軆?yōu)秀還一直在持續(xù)努力變得更優(yōu)秀篓吁,而你是不是還在滿足于現(xiàn)狀內(nèi)心在竊喜!希望讀到這的您能點(diǎn)個(gè)小贊和關(guān)注下我蚪拦,以后還會(huì)更新技術(shù)干貨杖剪,謝謝您的支持!

資料領(lǐng)取方式:加入Java技術(shù)交流群963944895驰贷,私信管理員即可免費(fèi)領(lǐng)取

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末盛嘿,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子括袒,更是在濱河造成了極大的恐慌次兆,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,284評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件锹锰,死亡現(xiàn)場(chǎng)離奇詭異芥炭,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)恃慧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門园蝠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人痢士,你說(shuō)我怎么就攤上這事彪薛。” “怎么了良瞧?”我有些...
    開(kāi)封第一講書人閱讀 164,614評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵陪汽,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我褥蚯,道長(zhǎng)挚冤,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 58,671評(píng)論 1 293
  • 正文 為了忘掉前任赞庶,我火速辦了婚禮训挡,結(jié)果婚禮上澳骤,老公的妹妹穿的比我還像新娘。我一直安慰自己澜薄,他們只是感情好为肮,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著肤京,像睡著了一般颊艳。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上忘分,一...
    開(kāi)封第一講書人閱讀 51,562評(píng)論 1 305
  • 那天棋枕,我揣著相機(jī)與錄音,去河邊找鬼妒峦。 笑死重斑,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的肯骇。 我是一名探鬼主播窥浪,決...
    沈念sama閱讀 40,309評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼笛丙!你這毒婦竟也來(lái)了漾脂?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 39,223評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤胚鸯,失蹤者是張志新(化名)和其女友劉穎符相,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體蠢琳,經(jīng)...
    沈念sama閱讀 45,668評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評(píng)論 3 336
  • 正文 我和宋清朗相戀三年镜豹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了傲须。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,981評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡趟脂,死狀恐怖泰讽,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情昔期,我是刑警寧澤已卸,帶...
    沈念sama閱讀 35,705評(píng)論 5 347
  • 正文 年R本政府宣布,位于F島的核電站硼一,受9級(jí)特大地震影響累澡,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜般贼,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評(píng)論 3 330
  • 文/蒙蒙 一愧哟、第九天 我趴在偏房一處隱蔽的房頂上張望奥吩。 院中可真熱鬧,春花似錦蕊梧、人聲如沸霞赫。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,904評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)端衰。三九已至,卻和暖如春甘改,著一層夾襖步出監(jiān)牢的瞬間旅东,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,023評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工楼誓, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留玉锌,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,146評(píng)論 3 370
  • 正文 我出身青樓疟羹,卻偏偏與公主長(zhǎng)得像主守,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子榄融,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評(píng)論 2 355

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