聊聊常用設(shè)計(jì)模式(三)單例模式之餓漢式和懶漢式

概述

?單例模式(Singleton Pattern)是確保一個(gè)類在任何情況下都絕對(duì)只有一個(gè)實(shí)例,并提供一個(gè)全局訪問點(diǎn)。單例模式是創(chuàng)建型模式。J2EE的標(biāo)準(zhǔn)的ServletContext、ServletContextConfig等、Spring框架應(yīng)用中的ApplicationContext、數(shù)據(jù)庫連接池等也都是單例模式。

1.餓漢式單例模式

先來看看類結(jié)構(gòu)圖:


類結(jié)構(gòu)圖

?餓漢式單例模式在類加載的時(shí)候就立刻初始化,并且創(chuàng)建單例對(duì)象。他絕對(duì)線程安全,在線程出現(xiàn)之前就已經(jīng)實(shí)例化,不存在安全訪問問題。

優(yōu)點(diǎn):沒有添加任何鎖、執(zhí)行效率高,用戶體驗(yàn)比懶漢式單例模式更好。
缺點(diǎn):類加載時(shí)就初始化,會(huì)造成內(nèi)存浪費(fèi)。(內(nèi)存換效率)

來看下代碼:

/**
 * @Author: zhouzhen
 * @email: zhouzhen0517@foxmail.com
 * @Description 提供一個(gè)全局訪問點(diǎn),并隱藏所有的構(gòu)造方法
 * 餓漢式單例 優(yōu)點(diǎn):執(zhí)行效率高,沒有任何鎖
 * 缺點(diǎn): 會(huì)造成內(nèi)存浪費(fèi)
 * @Date: Create in 10:34 2020/4/12
 */
public class HungrySingleton {

    private static final HungrySingleton hungrySingleton = new HungrySingleton();

    private HungrySingleton() {
    }

    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
}

靜態(tài)代碼塊的實(shí)現(xiàn)方式:

/**
 * @Author: zhouzhen
 * @email: zhouzhen0517@foxmail.com
 * @Description
 * @Date: Create in 10:46 2020/4/12
 */
public class HungryStaticSingleton {

    private static final HungryStaticSingleton hungryStaticSingleton;

    static {
        hungryStaticSingleton = new HungryStaticSingleton();
    }

    private HungryStaticSingleton() {
    }

    public static HungryStaticSingleton getInstance() {
        return hungryStaticSingleton;
    }


}

?兩種寫法并沒有本質(zhì)上的區(qū)別,餓漢式單例模式適用于單例對(duì)象較少的情況。下面我們來看性能更優(yōu)的寫法。

2.懶漢式單例模式

?懶漢式單例模式的特點(diǎn)是:只有被外部類調(diào)用的時(shí)候才會(huì)生成對(duì)應(yīng)單例對(duì)象的實(shí)例。下面看懶漢式單例模式的簡單實(shí)現(xiàn)LazySimpleSingleton:

/**
 * @Author: zhouzhen
 * @email: zhouzhen0517@foxmail.com
 * @Description
 * @Date: Create in 11:17 2020/4/12
 */
public class LazySimpleSingleton {

    private static LazySimpleSingleton instance;

    private LazySimpleSingleton() {
    }

    public static LazySimpleSingleton getInstance() {
        if(instance == null) {
            //打印當(dāng)前創(chuàng)建的實(shí)例
            System.out.println(Thread.currentThread().getName() + ":"
                    + (instance = new LazySimpleSingleton()));
        }
        return instance;
    }

}

然后寫一個(gè)線程類ExectorThread:

/**
 * @Author: zhouzhen
 * @email: zhouzhen0517@foxmail.com
 * @Description
 * @Date: Create in 22:37 2020/4/12
 */
public class ExectorThread implements Runnable {
    public void run() {
        LazySimpleSingleton.getInstance();
    }
}

調(diào)用代碼:

/**
 * @Author: zhouzhen
 * @email: zhouzhen0517@foxmail.com
 * @Description
 * @Date: Create in 22:40 2020/4/12
 */
public class LazySimpleSingletonTest {

    public static void main(String[] args) {
        Thread t1 = new Thread(new ExectorThread());
        Thread t2 = new Thread(new ExectorThread());
        t1.start();
        t2.start();
    }
}

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


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

?可以發(fā)現(xiàn),在并發(fā)情況下,上述代碼有一定概率會(huì)創(chuàng)建兩個(gè)不同的對(duì)象,破壞了實(shí)例對(duì)象的全局單例,存在線程安全隱患。下面我們用synchronized關(guān)鍵字來保證getInstance()的線程安全。

/**
 * @Author: zhouzhen
 * @email: zhouzhen0517@foxmail.com
 * @Description
 * @Date: Create in 11:17 2020/4/12
 */
public class LazySimpleSingleton {

    private static LazySimpleSingleton instance;

    private LazySimpleSingleton() {
    }

    public synchronized static LazySimpleSingleton getInstance() {
        if(instance == null) {
            System.out.println(Thread.currentThread().getName() + ":"
                    + (instance = new LazySimpleSingleton()));
        }
        return instance;
    }

}

?通過上述方法,線程安全問題解決了,但在線程數(shù)量比較多的情況下,CPU分配壓力上升,則會(huì)導(dǎo)致大批線程阻塞,導(dǎo)致程序性能大幅下降。如何解決這個(gè)問題呢,來看看雙重檢查鎖的單例模式:

/**
 * @Author: zhouzhen
 * @email: zhouzhen0517@foxmail.com
 * @Description
 * @Date: Create in 11:17 2020/4/12
 */
public class LazySimpleSingleton {

    private volatile static LazySimpleSingleton instance;

    private LazySimpleSingleton() {
    }

    public synchronized static LazySimpleSingleton getInstance() {
        if(instance == null) {
            synchronized(LazySimpleSingleton.class) {
                if(instance == null) {
                    System.out.println(Thread.currentThread().getName() + ":"
                            + (instance = new LazySimpleSingleton()));
                }
            }
        }
        return instance;
    }

?當(dāng)?shù)谝粋€(gè)線程調(diào)用getInstance()方法時(shí),第二個(gè)線程也可以調(diào)用。當(dāng)?shù)谝粋€(gè)線程執(zhí)行到synchronized時(shí)會(huì)上鎖,第二個(gè)線程就會(huì)變成阻塞狀態(tài)。此時(shí)的阻塞不同于之前整個(gè)類的阻塞,而是在getInstance()方法內(nèi)部的阻塞,對(duì)于調(diào)用者的性能影響感知不強(qiáng)。
?在instance前添加volatile 關(guān)鍵字是為了解決指令重排序問題,可以參考https://blog.csdn.net/hl_java/article/details/89160086
?當(dāng)然,用到synchronized關(guān)鍵字總歸要上鎖,對(duì)程序性能還是存在影響的。來看從類的初始化角度考慮的,采用靜態(tài)內(nèi)部類的方法實(shí)現(xiàn)的終極解決方案:

/**
 * @Author: zhouzhen
 * @email: zhouzhen0517@foxmail.com
 * @Description
 * @Date: Create in 23:08 2020/4/12
 */
public class LazyInnerClassSingleton {
    //使用LazyInnerClassGeneral的時(shí)候,默認(rèn)會(huì)先初始化內(nèi)部類
    //如果沒有被使用,則內(nèi)部類是不被加載的
    private LazyInnerClassSingleton() {
    }

    //static保證了單例的空間共享,final保證這個(gè)方法不會(huì)被重寫和重載
    public static final LazyInnerClassSingleton getInstance() {
        //在返回結(jié)果之前,一定會(huì)先加載內(nèi)部類
        return LazyHolder.LAZY;
    }

    //默認(rèn)不加載
    private static class LazyHolder {
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

?靜態(tài)內(nèi)部類在ClassPath下的形式是LazyInnerClassSingleton$LazyHolder.class的形式,LazyInnerClassSingleton類加載時(shí)不會(huì)第一時(shí)間掃描內(nèi)部類,只有在getInstance()方法被調(diào)用時(shí),內(nèi)部類才會(huì)初始化,然后生成實(shí)例。這種方式兼顧了懶漢式單例模式的內(nèi)存浪費(fèi)問題和synchronized的性能問題,巧妙的避免了線程安全問題。

文章參考

《Spring5核心原理》〔中〕譚勇德(Tom)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容