設(shè)計(jì)模式系列--單例模式

單例模式介紹

單例模式是設(shè)計(jì)模式中最常見也最簡(jiǎn)單的一種設(shè)計(jì)模式亚铁,單例模式的主要作用是保證在Java程序中失息,某個(gè)類只有一個(gè)實(shí)例存在氯质。一些管理器和控制器常被設(shè)計(jì)成單例模式募舟。比如在android實(shí)際APP 開發(fā)中用到的 賬號(hào)信息對(duì)象管理, 數(shù)據(jù)庫對(duì)象(SQLiteOpenHelper)等都會(huì)用到單例模式闻察。

單例模式有很多好處拱礁,它能夠避免實(shí)例對(duì)象的重復(fù)創(chuàng)建,不僅可以減少每次創(chuàng)建對(duì)象的時(shí)間開銷辕漂,還可以節(jié)約內(nèi)存空間呢灶;能夠避免由于操作多個(gè)實(shí)例導(dǎo)致的邏輯錯(cuò)誤。如果一個(gè)對(duì)象有可能貫穿整個(gè)應(yīng)用程序钉嘹,而且起到了全局統(tǒng)一管理控制的作用填抬,那么單例模式也許是一個(gè)值得考慮的選擇。

單例模式有很多種寫法隧期,大部分寫法都或多或少有一些不足飒责。下面將分別對(duì)這幾種寫法進(jìn)行介紹。

單例模式寫法

  1. 餓漢模式
public class Singleton{  
    private static Singleton instance = new Singleton();  
    private Singleton(){}  
    public static Singleton newInstance(){  
        return instance;  
    }  
}  

從代碼中我們看到仆潮,類的構(gòu)造函數(shù)定義為private的宏蛉,保證其他類不能實(shí)例化此類,然后提供了一個(gè)靜態(tài)實(shí)例并返回給調(diào)用者性置。餓漢模式是最簡(jiǎn)單的一種實(shí)現(xiàn)方式拾并,餓漢模式在類加載的時(shí)候就對(duì)實(shí)例進(jìn)行創(chuàng)建,實(shí)例在整個(gè)程序周期都存在鹏浅。它的好處是只在類加載的時(shí)候創(chuàng)建一次實(shí)例嗅义,不會(huì)存在多個(gè)線程創(chuàng)建多個(gè)實(shí)例的情況,避免了多線程同步的問題隐砸。它的缺點(diǎn)也很明顯之碗,即使這個(gè)單例沒有用到也會(huì)被創(chuàng)建,而且在類加載之后就被創(chuàng)建季希,內(nèi)存就被浪費(fèi)了褪那。
這種實(shí)現(xiàn)方式適合單例占用內(nèi)存比較小,在初始化時(shí)就會(huì)被用到的情況式塌。但是博敬,如果單例占用的內(nèi)存比較大,或單例只是在某個(gè)特定場(chǎng)景下才會(huì)用到峰尝,使用餓漢模式就不合適了偏窝,這時(shí)候就需要用到懶漢模式進(jìn)行延遲加載。

  1. 懶漢模式
public class Singleton{  
 //持有私有靜態(tài)實(shí)例武学,防止被引用祭往,此處賦值為null,目的是實(shí)現(xiàn)延遲加載 
    private static Singleton instance = null;  
  /* 私有構(gòu)造方法劳淆,防止被實(shí)例化 */  
    private Singleton(){}  
 /* 1:懶漢式链沼,靜態(tài)工程方法,創(chuàng)建實(shí)例 */ 
    public static Singleton newInstance(){  
        if(null == instance){  
            instance = new Singleton();  
        }  
        return instance;  
    }  
}

懶漢模式中單例是在需要的時(shí)候才去創(chuàng)建的沛鸵,如果單例已經(jīng)創(chuàng)建括勺,再次調(diào)用獲取接口將不會(huì)重新創(chuàng)建新的對(duì)象,而是直接返回之前創(chuàng)建的對(duì)象曲掰。如果某個(gè)單例使用的次數(shù)少疾捍,并且創(chuàng)建單例消耗的資源較多,那么就需要實(shí)現(xiàn)單例的按需創(chuàng)建栏妖,這個(gè)時(shí)候使用懶漢模式就是一個(gè)不錯(cuò)的選擇乱豆。但是這里的懶漢模式并沒有考慮線程安全問題,在多個(gè)線程可能會(huì)并發(fā)調(diào)用它的getInstance()方法吊趾,導(dǎo)致創(chuàng)建多個(gè)實(shí)例宛裕,因此需要加鎖解決線程同步問題瑟啃,實(shí)現(xiàn)如下。

public class Singleton{  

    private static Singleton instance = null;  
    private Singleton(){}  
    public static synchronized Singleton newInstance(){  
        if(null == instance){  
            instance = new Singleton();  
        }  
        return instance;  
    }  
} 

優(yōu)點(diǎn):解決了線程不安全的問題揩尸。
缺點(diǎn):效率有點(diǎn)低蛹屿,每次調(diào)用實(shí)例都要判斷同步鎖

  1. 雙重校驗(yàn)鎖
    加鎖的懶漢模式看起來即解決了線程并發(fā)問題,又實(shí)現(xiàn)了延遲加載岩榆,然而它存在著性能問題错负,依然不夠完美。synchronized修飾的同步方法比一般方法要慢很多勇边,如果多次調(diào)用getInstance()犹撒,累積的性能損耗就比較大了。因此就有了雙重校驗(yàn)鎖粒褒,先看下它的實(shí)現(xiàn)代碼识颊。
public class Singleton {  
    private static Singleton instance = null;  
    private Singleton(){}  
    public static Singleton getInstance() {  
        if (instance == null) {  
            synchronized (Singleton.class) {  
                if (instance == null) {//2  
                    instance = new Singleton();  
                }  
            }  
        }  
        return instance;  
    }  
} 

可以看到上面在同步代碼塊外多了一層instance為空的判斷。由于單例對(duì)象只需要?jiǎng)?chuàng)建一次怀浆,如果后面再次調(diào)用getInstance()只需要直接返回單例對(duì)象谊囚。因此,大部分情況下执赡,調(diào)用getInstance()都不會(huì)執(zhí)行到同步代碼塊镰踏,從而提高了程序性能。不過還需要考慮一種情況沙合,假如兩個(gè)線程A奠伪、B,A執(zhí)行了if (instance == null)語句首懈,它會(huì)認(rèn)為單例對(duì)象沒有創(chuàng)建绊率,此時(shí)線程切到B也執(zhí)行了同樣的語句,B也認(rèn)為單例對(duì)象沒有創(chuàng)建究履,然后兩個(gè)線程依次執(zhí)行同步代碼塊滤否,并分別創(chuàng)建了一個(gè)單例對(duì)象。為了解決這個(gè)問題最仑,還需要在同步代碼塊中增加if (instance == null)語句藐俺,也就是上面看到的代碼2。
我們看到雙重校驗(yàn)鎖即實(shí)現(xiàn)了延遲加載泥彤,又解決了線程并發(fā)問題欲芹,同時(shí)還解決了執(zhí)行效率問題,是否真的就萬無一失了呢吟吝?
這里要提到Java中的指令重排優(yōu)化菱父。所謂指令重排優(yōu)化是指在不改變?cè)Z義的情況下,通過調(diào)整指令的執(zhí)行順序讓程序運(yùn)行的更快。JVM中并沒有規(guī)定編譯器優(yōu)化相關(guān)的內(nèi)容浙宜,也就是說JVM可以自由的進(jìn)行指令重排序的優(yōu)化官辽。
這個(gè)問題的關(guān)鍵就在于由于指令重排優(yōu)化的存在,導(dǎo)致初始化Singleton和將對(duì)象地址賦給instance字段的順序是不確定的粟瞬。在某個(gè)線程創(chuàng)建單例對(duì)象時(shí)野崇,在構(gòu)造方法被調(diào)用之前,就為該對(duì)象分配了內(nèi)存空間并將對(duì)象的字段設(shè)置為默認(rèn)值亩钟。此時(shí)就可以將分配的內(nèi)存地址賦值給instance字段了,然而該對(duì)象可能還沒有初始化鳖轰。若緊接著另外一個(gè)線程來調(diào)用getInstance清酥,取到的就是狀態(tài)不正確的對(duì)象,程序就會(huì)出錯(cuò)蕴侣。
以上就是雙重校驗(yàn)鎖會(huì)失效的原因焰轻,不過還好在JDK1.5及之后版本增加了volatile關(guān)鍵字。volatile的一個(gè)語義是禁止指令重排序優(yōu)化昆雀,也就保證了instance變量被賦值的時(shí)候?qū)ο笠呀?jīng)是初始化過的辱志,從而避免了上面說到的問題。代碼如下:

public class Singleton {  
    private static volatile Singleton instance = null;  
    private Singleton(){}  
    public static Singleton getInstance() {  
        if (instance == null) {  
            synchronized (Singleton.class) {  
                if (instance == null) {  
                    instance = new Singleton();  
                }  
            }  
        }  
        return instance;  
    }  
} 
  1. 靜態(tài)內(nèi)部類
public class Singleton{  
    private static class SingletonHolder{  
        public static Singleton instance = new Singleton();  
    }  
    private Singleton(){}  
    public static Singleton newInstance(){  
        return SingletonHolder.instance;  
    }  
} 

這種方式同樣利用了類加載機(jī)制來保證只創(chuàng)建一個(gè)instance實(shí)例狞膘。它與餓漢模式一樣揩懒,也是利用了類加載機(jī)制,因此不存在多線程并發(fā)的問題挽封。不一樣的是已球,它是在內(nèi)部類里面去創(chuàng)建對(duì)象實(shí)例。這樣的話辅愿,只要應(yīng)用中不使用內(nèi)部類智亮,JVM就不會(huì)去加載這個(gè)單例類,也就不會(huì)創(chuàng)建單例對(duì)象点待,從而實(shí)現(xiàn)懶漢式的延遲加載阔蛉。也就是說這種方式可以同時(shí)保證延遲加載和線程安全。

  1. 枚舉
public enum Singleton{  
    instance;  
    public void whateverMethod(){}      
}  

上面提到的四種實(shí)現(xiàn)單例的方式都有共同的缺點(diǎn):
1)需要額外的工作來實(shí)現(xiàn)序列化癞埠,否則每次反序列化一個(gè)序列化的對(duì)象時(shí)都會(huì)創(chuàng)建一個(gè)新的實(shí)例状原。
2)可以使用反射強(qiáng)行調(diào)用私有構(gòu)造器(如果要避免這種情況,可以修改構(gòu)造器燕差,讓它在創(chuàng)建第二個(gè)實(shí)例的時(shí)候拋異常)遭笋。
而枚舉類很好的解決了這兩個(gè)問題,使用枚舉除了線程安全和防止反射調(diào)用構(gòu)造器之外徒探,還提供了自動(dòng)序列化機(jī)制瓦呼,防止反序列化的時(shí)候創(chuàng)建新的對(duì)象。因此,《Effective Java》作者推薦使用的方法央串。不過磨澡,在實(shí)際工作中,很少看見有人這么寫质和。

總結(jié)
本文總結(jié)了五種Java中實(shí)現(xiàn)單例的方法稳摄,其中前兩種都不夠完美,雙重校驗(yàn)鎖和靜態(tài)內(nèi)部類的方式可以解決大部分問題饲宿,平時(shí)工作中使用的最多的也是這兩種方式厦酬。枚舉方式雖然很完美的解決了各種問題,但是這種寫法多少讓人感覺有些生疏瘫想。個(gè)人的建議是仗阅,在沒有特殊需求的情況下,使用第三種和第四種方式實(shí)現(xiàn)單例模式国夜。

在android中使用內(nèi)存泄漏問題

因?yàn)閱卫撵o態(tài)特性使得單例的生命周期和應(yīng)用的生命周期一樣長(zhǎng)减噪, 這就說明了如果一個(gè)對(duì)象已經(jīng)不需要使用了,而單例對(duì)象還持有該對(duì)象的引用车吹,那么這個(gè)對(duì)象將不能被正吵镌#回收,這就導(dǎo)致了內(nèi)存泄漏窄驹。

Android中習(xí)慣使用單例的常見類: xxxManager , xxxHelper , xxxUtils 等
而造成泄漏的原因:
1.是擁有更長(zhǎng)生命周期的對(duì)象持有短生命周期對(duì)象的強(qiáng)引用朝卒。
2.單例對(duì)象的生命周期是和應(yīng)用一樣的,所以如果它沒有持有比它生命周期更短的對(duì)象的引用自然沒事馒吴。
需要注意單例中持有對(duì)象的生命周期扎运,防止內(nèi)存泄漏。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末饮戳,一起剝皮案震驚了整個(gè)濱河市豪治,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌扯罐,老刑警劉巖负拟,帶你破解...
    沈念sama閱讀 221,820評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異歹河,居然都是意外死亡掩浙,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門秸歧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來厨姚,“玉大人,你說我怎么就攤上這事键菱∶剑” “怎么了?”我有些...
    開封第一講書人閱讀 168,324評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)拭抬。 經(jīng)常有香客問我部默,道長(zhǎng),這世上最難降的妖魔是什么造虎? 我笑而不...
    開封第一講書人閱讀 59,714評(píng)論 1 297
  • 正文 為了忘掉前任傅蹂,我火速辦了婚禮,結(jié)果婚禮上算凿,老公的妹妹穿的比我還像新娘份蝴。我一直安慰自己,他們只是感情好氓轰,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,724評(píng)論 6 397
  • 文/花漫 我一把揭開白布搞乏。 她就那樣靜靜地躺著,像睡著了一般戒努。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上镐躲,一...
    開封第一講書人閱讀 52,328評(píng)論 1 310
  • 那天储玫,我揣著相機(jī)與錄音,去河邊找鬼萤皂。 笑死撒穷,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的裆熙。 我是一名探鬼主播端礼,決...
    沈念sama閱讀 40,897評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼入录!你這毒婦竟也來了蛤奥?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,804評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤僚稿,失蹤者是張志新(化名)和其女友劉穎凡桥,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蚀同,經(jīng)...
    沈念sama閱讀 46,345評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡缅刽,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,431評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蠢络。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片衰猛。...
    茶點(diǎn)故事閱讀 40,561評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖刹孔,靈堂內(nèi)的尸體忽然破棺而出啡省,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 36,238評(píng)論 5 350
  • 正文 年R本政府宣布冕杠,位于F島的核電站微姊,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏分预。R本人自食惡果不足惜兢交,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,928評(píng)論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望笼痹。 院中可真熱鬧配喳,春花似錦、人聲如沸凳干。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽救赐。三九已至涧团,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間经磅,已是汗流浹背泌绣。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留预厌,地道東北人阿迈。 一個(gè)月前我還...
    沈念sama閱讀 48,983評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像轧叽,于是被迫代替她去往敵國(guó)和親苗沧。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,573評(píng)論 2 359

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