單例模式詳解

單例模式的定義

單例模式就是在程序運(yùn)行中只實(shí)例化一次重归,創(chuàng)建一個(gè)全局唯一對象桅狠,有點(diǎn)像 Java 的靜態(tài)變量奸笤,但是單例模式要優(yōu)于靜態(tài)變量痕檬,靜態(tài)變量在程序啟動的時(shí)候JVM就會進(jìn)行加載冤寿,如果不使用歹苦,會造成大量的資源浪費(fèi),單例模式能夠?qū)崿F(xiàn)懶加載督怜,能夠在使用實(shí)例的時(shí)候才去創(chuàng)建實(shí)例殴瘦。開發(fā)工具類庫中的很多工具類都應(yīng)用了單例模式,比如線程池号杠、緩存蚪腋、日志對象等,它們都只需要?jiǎng)?chuàng)建一個(gè)對象究流,如果創(chuàng)建多份實(shí)例辣吃,可能會帶來不可預(yù)知的問題,比如資源的浪費(fèi)芬探、結(jié)果處理不一致等問題神得。

單例的實(shí)現(xiàn)思路

靜態(tài)化實(shí)例對象
私有化構(gòu)造方法,禁止通過構(gòu)造方法創(chuàng)建實(shí)例
提供一個(gè)公共的靜態(tài)方法偷仿,用來返回唯一實(shí)例

單例的好處

只有一個(gè)對象哩簿,內(nèi)存開支少、性能好
避免對資源的多重占用
在系統(tǒng)設(shè)置全局訪問點(diǎn)酝静,優(yōu)化和共享資源訪問

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

單例模式的寫法有餓漢模式节榜、懶漢模式、雙重檢查鎖模式别智、靜態(tài)內(nèi)部類單例模式宗苍、枚舉類實(shí)現(xiàn)單例模式五種方式,其中懶漢模式、雙重檢查鎖模式讳窟,如果你寫法不當(dāng)让歼,在多線程情況下會存在不是單例或者單例出異常等問題,具體的原因丽啡,在后面的對應(yīng)處會進(jìn)行說明谋右。我們從最基本的餓漢模式開始我們的單例編寫之路。

1. 餓漢模式

餓漢模式采用一種簡單粗暴的形式补箍,在定義靜態(tài)屬性時(shí)改执,直接實(shí)例化了對象。代碼如下:

//在類加載時(shí)就完成了初始化坑雅,所以類加載較慢辈挂,但獲取對象的速度快
public class SingletonObject1 {
    // 利用靜態(tài)變量來存儲唯一實(shí)例
    private static final SingletonObject1 instance = new SingletonObject1();
    // 私有化構(gòu)造函數(shù)
    private SingletonObject1(){
        // 里面可能有很多操作
    }
    // 提供公開獲取實(shí)例接口
    public static SingletonObject1 getInstance(){
        return instance;
    }
}

餓漢模式的優(yōu)缺點(diǎn):

  • 優(yōu)點(diǎn)
    由于使用了static關(guān)鍵字,保證了在引用這個(gè)變量時(shí)霞丧,關(guān)于這個(gè)變量的所有寫入操作都完成呢岗,所以保證了JVM層面的線程安全
  • 缺點(diǎn)
    不能實(shí)現(xiàn)懶加載,造成空間浪費(fèi)蛹尝,如果一個(gè)類比較大后豫,我們在初始化的時(shí)就加載了這個(gè)類,但是我們長時(shí)間沒有使用這個(gè)類突那,這就導(dǎo)致了內(nèi)存空間的浪費(fèi)挫酿。

2.懶漢模式

懶漢模式是一種偷懶的模式,在程序初始化時(shí)不會創(chuàng)建實(shí)例愕难,只有在使用實(shí)例的時(shí)候才會創(chuàng)建實(shí)例早龟,所以懶漢模式解決了餓漢模式帶來的空間浪費(fèi)問題,同時(shí)也引入了其他的問題猫缭,我們先來看看下面這個(gè)懶漢模式

public class SingletonObject2 {
    // 定義靜態(tài)變量時(shí)葱弟,未初始化實(shí)例
    private static SingletonObject2 instance;
    // 私有化構(gòu)造函數(shù)
    private SingletonObject2(){}
    public static SingletonObject2 getInstance(){
        // 使用時(shí),先判斷實(shí)例是否為空猜丹,如果實(shí)例為空芝加,則實(shí)例化對象
        if (instance == null)
            instance = new SingletonObject2();
        return instance;
    }
}

上面是懶漢模式的實(shí)現(xiàn)方式,但是上面這段代碼在多線程的情況下是不安全的射窒,因?yàn)樗荒鼙WC是單例模式藏杖,有可能會出現(xiàn)多份實(shí)例的情況,出現(xiàn)多份實(shí)例的情況是在創(chuàng)建實(shí)例對象時(shí)候造成的脉顿。所以我單獨(dú)把實(shí)例化的代碼提出蝌麸,來分析一下為什么會出現(xiàn)多份實(shí)例的情況。

     1   if (instance == null)
     2       instance = new SingletonObject2();

假設(shè)有兩個(gè)線程都進(jìn)入到 1 這個(gè)位置艾疟,因?yàn)闆]有任何資源保護(hù)措施来吩,所以兩個(gè)線程可以同時(shí)判斷的instance都為空敢辩,都將去執(zhí)行 2 的實(shí)例化代碼,所以就會出現(xiàn)多份實(shí)例的情況弟疆。
通過上面的分析我們已經(jīng)知道出現(xiàn)多份實(shí)例的原因责鳍,如果我們在創(chuàng)建實(shí)例的時(shí)候進(jìn)行資源保護(hù),是不是可以解決多份實(shí)例的問題兽间?確實(shí)如此,我們給getInstance()方法加上synchronized關(guān)鍵字正塌,使得getInstance()方法成為受保護(hù)的資源就能夠解決多份實(shí)例的問題嘀略。加上synchronized關(guān)鍵字之后代碼如下:

public class SingletonObject3 {
    private static SingletonObject3 instance;
    private SingletonObject3(){}
    public synchronized static SingletonObject3 getInstance(){
        /**
         * 添加class類鎖,影響了性能乓诽,加鎖之后將代碼進(jìn)行了串行化帜羊,
         * 我們的代碼塊絕大部分是讀操作,在讀操作的情況下鸠天,代碼線程是安全的
         */
        if (instance == null)
            instance = new SingletonObject3();
        return instance;
    }
}

經(jīng)過修改后讼育,解決了多份實(shí)例的問題,但是因?yàn)橐雜ynchronized關(guān)鍵字稠集,對代碼加了鎖奶段,就引入了新的問題,加鎖之后會使得程序變成串行化剥纷,只有搶到鎖的線程才能去執(zhí)行這段代碼塊痹籍,這會使得系統(tǒng)的性能大大下降。
懶漢模式的優(yōu)缺點(diǎn):

  • 優(yōu)點(diǎn)
    實(shí)現(xiàn)了懶加載晦鞋,節(jié)約了內(nèi)存空間
  • 缺點(diǎn)
    在不加鎖的情況下蹲缠,線程不安全,可能出現(xiàn)多份實(shí)例
    在加鎖的情況下悠垛,會是程序串行化线定,使系統(tǒng)有嚴(yán)重的性能問題

3. 雙重檢查鎖模式

再來討論一下懶漢模式中加鎖的問題,對于getInstance()方法來說确买,絕大部分的操作都是讀操作斤讥,讀操作是線程安全的,所以我們沒必讓每個(gè)線程必須持有鎖才能調(diào)用該方法拇惋,我們需要調(diào)整加鎖的問題周偎。由此也產(chǎn)生了一種新的實(shí)現(xiàn)模式:雙重檢查鎖模式,下面是雙重檢查鎖模式的單例實(shí)現(xiàn)代碼塊:

public class SingletonObject4 {
    private static SingletonObject4 instance;
    private SingletonObject4(){

    }
    public static SingletonObject4 getInstance(){
        // 第一次判斷撑帖,如果這里為空蓉坎,不進(jìn)入搶鎖階段,直接返回實(shí)例
        if (instance == null)
            synchronized (SingletonObject4.class){
                // 搶到鎖之后再次判斷是否為空
                if (instance == null){
                    instance = new SingletonObject4();
                }
            }
        return instance;
    }
}

雙重檢查鎖模式是一種非常好的單例實(shí)現(xiàn)模式胡嘿,解決了單例蛉艾、性能、線程安全問題,上面的雙重檢測鎖模式看上去完美無缺勿侯,其實(shí)是存在問題拓瞪,在多線程的情況下,可能會出現(xiàn)空指針問題助琐,出現(xiàn)問題的原因是JVM在實(shí)例化對象的時(shí)候會進(jìn)行優(yōu)化和指令重排序操作祭埂。什么是指令重排?兵钮,看下面這個(gè)例子蛆橡,簡單了解一下指令從排序

  private SingletonObject4(){
     1   int x = 10;
     2   int y = 30;
     3  Object o = new Object();  
    }

上面的構(gòu)造函數(shù)SingletonObject4(),我們編寫的順序是1掘譬、2泰演、3,JVM 會對它進(jìn)行指令重排序葱轩,所以執(zhí)行順序可能是3睦焕、1、2靴拱,也可能是2垃喊、3、1袜炕,不管是那種執(zhí)行順序缔御,JVM 最后都會保證所以實(shí)例都完成實(shí)例化。 如果構(gòu)造函數(shù)中操作比較多時(shí)妇蛀,為了提升效率耕突,JVM 會在構(gòu)造函數(shù)里面的屬性未全部完成實(shí)例化時(shí),就返回對象评架。雙重檢測鎖出現(xiàn)空指針問題的原因就是出現(xiàn)在這里眷茁,當(dāng)某個(gè)線程獲取鎖進(jìn)行實(shí)例化時(shí),其他線程就直接獲取實(shí)例使用纵诞,由于JVM指令重排序的原因上祈,其他線程獲取的對象也許不是一個(gè)完整的對象,所以在使用實(shí)例的時(shí)候就會出現(xiàn)空指針異常問題浙芙。
要解決雙重檢查鎖模式帶來空指針異常的問題登刺,只需要使用volatile關(guān)鍵字,volatile關(guān)鍵字嚴(yán)格遵循h(huán)appens-before原則嗡呼,即在讀操作前纸俭,寫操作必須全部完成。添加volatile關(guān)鍵字之后的單例模式代碼:

public class SingletonObject5 {
    // 添加volatile關(guān)鍵字
    private static volatile SingletonObject5 instance;
    private SingletonObject5(){

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

添加volatile關(guān)鍵字之后的雙重檢查鎖模式是一種比較好的單例實(shí)現(xiàn)模式南窗,能夠保證在多線程的情況下線程安全也不會有性能問題揍很。

4.靜態(tài)內(nèi)部類單例模式

靜態(tài)內(nèi)部類單例模式也稱單例持有者模式郎楼,實(shí)例由內(nèi)部類創(chuàng)建,由于 JVM 在加載外部類的過程中, 是不會加載靜態(tài)內(nèi)部類的, 只有內(nèi)部類的屬性/方法被調(diào)用時(shí)才會被加載, 并初始化其靜態(tài)屬性窒悔。靜態(tài)屬性由static修飾呜袁,保證只被實(shí)例化一次,并且嚴(yán)格保證實(shí)例化順序简珠。靜態(tài)內(nèi)部類單例模式代碼如下:

public class SingletonObject6 {
    private SingletonObject6(){
    }
    // 單例持有者
    private static class InstanceHolder{
        private  final static SingletonObject6 instance = new SingletonObject6();
    }
    public static SingletonObject6 getInstance(){
        // 調(diào)用內(nèi)部類屬性
        return InstanceHolder.instance;
    }
}

靜態(tài)內(nèi)部類單例模式是一種優(yōu)秀的單例模式阶界,是開源項(xiàng)目中比較常用的一種單例模式。在沒有加任何鎖的情況下聋庵,保證了多線程下的安全荐操,并且沒有任何性能影響和空間的浪費(fèi)。

5.枚舉類實(shí)現(xiàn)單例模式

枚舉類實(shí)現(xiàn)單例模式是 effective java 作者極力推薦的單例實(shí)現(xiàn)模式珍策,因?yàn)槊杜e類型是線程安全的,并且只會裝載一次宅倒,設(shè)計(jì)者充分的利用了枚舉的這個(gè)特性來實(shí)現(xiàn)單例模式攘宙,枚舉的寫法非常簡單拐迁,而且枚舉類型是所用單例實(shí)現(xiàn)中唯一一種不會被破壞的單例實(shí)現(xiàn)模式。

public class SingletonObject7 {
    private SingletonObject7(){

    }
    /**
     * 枚舉類型是線程安全的,并且只會裝載一次
     */
    private enum Singleton{
        INSTANCE;
        private final SingletonObject7 instance;
        Singleton(){
            instance = new SingletonObject7();
        }
        private SingletonObject7 getInstance(){
            return instance;
        }
    }
    public static SingletonObject7 getInstance(){
        return Singleton.INSTANCE.getInstance();
    }
}

破壞單例模式的方法及解決辦法

  1. 除枚舉方式外, 其他方法都會通過反射的方式破壞單例,反射是通過調(diào)用構(gòu)造方法生成新的對象讯壶,所以如果我們想要阻止單例破壞立轧,可以在構(gòu)造方法中進(jìn)行判斷氛改,若已有實(shí)例, 則阻止生成新的實(shí)例,解決辦法如下:
private SingletonObject1(){
    if (instance !=null){
        throw new RuntimeException("實(shí)例已經(jīng)存在瑰艘,請通過 getInstance()方法獲取");
    }
}
  1. 如果單例類實(shí)現(xiàn)了序列化接口Serializable, 就可以通過反序列化破壞單例,所以我們可以不實(shí)現(xiàn)序列化接口,如果非得實(shí)現(xiàn)序列化接口囤耳,可以重寫反序列化方法readResolve(), 反序列化時(shí)直接返回相關(guān)單例對象。
  public Object readResolve() throws ObjectStreamException {
        return instance;
    }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末材彪,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子雄嚣,更是在濱河造成了極大的恐慌蕴轨,老刑警劉巖封锉,帶你破解...
    沈念sama閱讀 221,820評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件荆残,死亡現(xiàn)場離奇詭異蕴潦,居然都是意外死亡忽冻,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評論 3 399
  • 文/潘曉璐 我一進(jìn)店門蹦骑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來边败,“玉大人缕坎,你說我怎么就攤上這事篡悟∶仗荆” “怎么了?”我有些...
    開封第一講書人閱讀 168,324評論 0 360
  • 文/不壞的土叔 我叫張陵搬葬,是天一觀的道長荷腊。 經(jīng)常有香客問我,道長急凰,這世上最難降的妖魔是什么女仰? 我笑而不...
    開封第一講書人閱讀 59,714評論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮抡锈,結(jié)果婚禮上疾忍,老公的妹妹穿的比我還像新娘。我一直安慰自己床三,他們只是感情好一罩,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,724評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著撇簿,像睡著了一般聂渊。 火紅的嫁衣襯著肌膚如雪差购。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,328評論 1 310
  • 那天汉嗽,我揣著相機(jī)與錄音欲逃,去河邊找鬼。 笑死诊胞,一個(gè)胖子當(dāng)著我的面吹牛暖夭,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播撵孤,決...
    沈念sama閱讀 40,897評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼迈着,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了邪码?” 一聲冷哼從身側(cè)響起裕菠,我...
    開封第一講書人閱讀 39,804評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎闭专,沒想到半個(gè)月后奴潘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,345評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡影钉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,431評論 3 340
  • 正文 我和宋清朗相戀三年画髓,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片平委。...
    茶點(diǎn)故事閱讀 40,561評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡奈虾,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出廉赔,到底是詐尸還是另有隱情肉微,我是刑警寧澤,帶...
    沈念sama閱讀 36,238評論 5 350
  • 正文 年R本政府宣布蜡塌,位于F島的核電站碉纳,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏馏艾。R本人自食惡果不足惜劳曹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,928評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望琅摩。 院中可真熱鬧厚者,春花似錦、人聲如沸迫吐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽志膀。三九已至熙宇,卻和暖如春鳖擒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背烫止。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評論 1 272
  • 我被黑心中介騙來泰國打工蒋荚, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人馆蠕。 一個(gè)月前我還...
    沈念sama閱讀 48,983評論 3 376
  • 正文 我出身青樓期升,卻偏偏與公主長得像,于是被迫代替她去往敵國和親互躬。 傳聞我的和親對象是個(gè)殘疾皇子播赁,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,573評論 2 359

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