日?qǐng)?bào)表格只有一份---單例模式

cover

前情提要

上集講到, 小光建立了開(kāi)分店的標(biāo)準(zhǔn)(工廠), 以后開(kāi)分店都按照這套標(biāo)準(zhǔn)執(zhí)行(從CompanyFactory的實(shí)現(xiàn)中生產(chǎn)開(kāi)分店的必須東西), 開(kāi)分店變得更加容易了.

小光也是馬上將自己的這套"開(kāi)分公司的工廠"投入使用了, 開(kāi)出了花山軟件新城分店.

隨著分店越來(lái)越多, 小光也請(qǐng)了分別請(qǐng)了店長(zhǎng)來(lái)"代理"小光之前的職責(zé). 當(dāng)然, 小光可不能完全放任不管啊, 他想著我至少得知道下每天各個(gè)店的基本情況吧.

所有示例源碼已經(jīng)上傳到Github, 戳這里

日?qǐng)?bào)制度

小光想到了當(dāng)時(shí)做程序猿時(shí), 敏捷開(kāi)發(fā)每天站立會(huì)議的三個(gè)問(wèn)題:

  1. 昨天完成了什么
  2. 今天要做什么
  3. 有什么困難, 阻力

心想, 我也可以根據(jù)這個(gè)弄個(gè)日?qǐng)?bào)制度啊, 讓各個(gè)店長(zhǎng)按照這個(gè)格式匯報(bào)下當(dāng)天的戰(zhàn)績(jī):


表格:

public class Form {

    private ArrayList<String> mFormData = new ArrayList<>();

    public void write(String data) {
        mFormData.add(data);
    }

    @Override
    public String toString() {
        return "表格:" + this.hashCode() + ", 數(shù)據(jù):" + mFormData;
    }
}

每天讓各個(gè)店子在打烊之前在系統(tǒng)中拿當(dāng)日的表格(如果還沒(méi)有, 就創(chuàng)建一個(gè))來(lái)填寫數(shù)據(jù), 然后提交.

出了問(wèn)題

想法挺好, 但是剛剛用上, 就出了問(wèn)題:

光谷店店長(zhǎng)表妹登錄系統(tǒng), 發(fā)現(xiàn)2016-12-16這個(gè)文件夾中還沒(méi)有表格文件, 于是本地創(chuàng)建了一個(gè), 用來(lái)填寫數(shù)據(jù), 準(zhǔn)備稍后提交. 然而此時(shí), 花山店的店長(zhǎng)小章也登錄了系統(tǒng), 也發(fā)現(xiàn)還沒(méi)有表格文件, 也創(chuàng)建了一個(gè)...

讓我們來(lái)看下操作:

表妹:

public class Cousins {

    public Form submitReport() {
        Form form = new Form();
        form.write("光谷店數(shù)據(jù)");
        return form;
    }
}

小章:

public class XiaoZhang {

    public Form submitReport() {
        Form form = new Form();
        form.write("花山店數(shù)據(jù)");
        return form;
    }
}

兩人的使用流程:

public class Demo {

    public static void main(String[] args) {

        Cousins cousins = new Cousins();
        Form form = cousins.submitReport();
        System.out.println(form);

        XiaoZhang xiaoZhang = new XiaoZhang();
        Form form2 = xiaoZhang.submitReport();
        System.out.println(form2);
    }
}

來(lái)看下結(jié)果:

表格:1639705018, 數(shù)據(jù):[光谷店數(shù)據(jù)]
表格:1627674070, 數(shù)據(jù):[花山店數(shù)據(jù)]

最終這個(gè)文件夾中有了兩個(gè)(不同的)表格, 小光看起來(lái)很是不方便...

表格應(yīng)該只能有一份

表格只能有一份, 小光心想. 那么怎么保證呢, 很簡(jiǎn)單, 我提前給創(chuàng)建好, 大家通過(guò)統(tǒng)一的接口來(lái)取這個(gè)文件, 而不能自己創(chuàng)建. 這樣就不會(huì)有問(wèn)題了:

public class HungryForm extends Form {

    // 提前創(chuàng)建好
    private static HungryForm sInstance = new HungryForm();

    // 私有化的構(gòu)造, 避免別人直接創(chuàng)建表格
    private HungryForm() {}

    // 店長(zhǎng)們通過(guò)這個(gè)接口來(lái)取表格
    public static HungryForm getInstance() {
        return sInstance;
    }
}

店長(zhǎng)們這樣提交報(bào)告:

public class Cousins {

    public Form submitReport() {
        // 直接新建一個(gè)表格
        // Form form = new Form();

        // 從固定的接口取表格
        Form form = HungryForm.getInstance();
        form.write("光谷店數(shù)據(jù)");
        return form;
    }
}

public class XiaoZhang {

    public Form submitReport() {
        // 直接新建一個(gè)表格
        // Form form = new Form();

        // 從固定的接口取表格
        Form form = HungryForm.getInstance();
        form.write("花山店數(shù)據(jù)");
        return form;
    }
}

提交方式不變:

public class Demo {

    public static void main(String[] args) {

        Cousins cousins = new Cousins();
        Form form = cousins.submitReport();
        System.out.println(form);

        XiaoZhang xiaoZhang = new XiaoZhang();
        Form form2 = xiaoZhang.submitReport();
        System.out.println(form2);
    }
}

來(lái)看下現(xiàn)在的結(jié)果:

表格:1639705018, 數(shù)據(jù):[光谷店數(shù)據(jù)]
表格:1639705018, 數(shù)據(jù):[光谷店數(shù)據(jù), 花山店數(shù)據(jù)]

可以看到兩人用的是同一份表格(hashCode一樣的), 生成的數(shù)據(jù)也沒(méi)有問(wèn)題了.

故事之后

看到這, 同學(xué)們應(yīng)該都看出來(lái)了, 小光這就是使用了大名鼎鼎的單例模式.
照例, 看下類圖, 這個(gè)應(yīng)該是最簡(jiǎn)單的類圖了:

單例模式
保證一個(gè)類(HungryForm)僅有一個(gè)實(shí)例(sInstance), 并提供一個(gè)訪問(wèn)該實(shí)例的全局訪問(wèn)點(diǎn)(getInstance).
這就意味著單例通常有如下兩個(gè)特點(diǎn):

  1. 構(gòu)造函數(shù)是私有的(避免別的地方創(chuàng)建它)
  2. 有一個(gè)static的方法來(lái)對(duì)外提供一個(gè)該單例的實(shí)例.

擴(kuò)展閱讀一

同學(xué)們可能注意到了, 我們?cè)谶@個(gè)單例模式中使用了Hungry這個(gè)詞, 沒(méi)錯(cuò), 我們這里實(shí)現(xiàn)單例的方式使用的就是餓漢式.

1, 餓漢式單例

餓漢式單例
顧名思義, 就是很餓, 不管三七二十一先創(chuàng)建了一個(gè)實(shí)例放著, 而不管最終用不用.

然而, 這個(gè)單例可能最終并不需要, 如果提前就創(chuàng)建好, 就會(huì)浪費(fèi)內(nèi)存空間了.
例如, 我們這個(gè)故事中, 年底假期中, 所有店子都歇業(yè)十天, 這十天就沒(méi)有任何店長(zhǎng)會(huì)去訪問(wèn)這個(gè)表格, 然而小光還是都每天都創(chuàng)建了, 這就造成了空間浪費(fèi)(假設(shè)這個(gè)表格數(shù)據(jù)(對(duì)象實(shí)例)很大...)

2, 懶漢式單例

那么怎么辦呢?
我們可以使用懶漢式單例:

public class LazyForm extends Form {

    private static LazyForm sInstance;

    // 私有化的構(gòu)造, 避免別人直接創(chuàng)建表格
    private LazyForm() {}

    // 店長(zhǎng)們通過(guò)這個(gè)接口來(lái)取表格
    public static LazyForm getInstance() {

        // 在有店長(zhǎng)訪問(wèn)該文件時(shí)才創(chuàng)建, 通過(guò)判斷當(dāng)前文件是否存在(sInstance == null)來(lái)避免重復(fù)創(chuàng)建
        if (sInstance == null) {
            sInstance = new LazyForm();
        }
        return sInstance;
    }
}

懶漢式單例
"懶", 也就是現(xiàn)在懶得創(chuàng)建, 等有用戶要用的時(shí)候才創(chuàng)建.

3, 線程安全的懶漢式單例

但是這樣創(chuàng)建也會(huì)有問(wèn)題啊, 因?yàn)樗峭ㄟ^(guò)sInstance == null判斷當(dāng)前是否已經(jīng)存在表格文件的, 假設(shè)有兩個(gè)店長(zhǎng)同時(shí)調(diào)用getInstance來(lái)取文件, 同時(shí)走到sInstance == null判斷這一步, 就會(huì)出問(wèn)題了 --- 有可能創(chuàng)建了兩個(gè)文件(實(shí)例), 就達(dá)不到單例的目的了.

所以說(shuō)這種懶漢式是線程不安全的, 在多線程環(huán)境下, 并不能做到單例.

那么, 該如何做, 既能懶加載, 又線程安全呢?
我們都知道Java中多線程環(huán)境往往會(huì)用到synchronized關(guān)鍵字, 通過(guò)他來(lái)做線程并發(fā)性控制.

synchronized方法控制對(duì)類成員變量的訪問(wèn), 每個(gè)類實(shí)例對(duì)應(yīng)一把鎖, synchronized修飾的方法必須獲得調(diào)用該方法的類實(shí)例的鎖方能執(zhí)行, 否則所屬線程阻塞. 方法一旦執(zhí)行, 就獨(dú)占該鎖. 直到從該方法返回時(shí)才將鎖釋放. 此后被阻塞的線程方能獲得該鎖, 重新進(jìn)入可執(zhí)行狀態(tài).

讓我們來(lái)看下線程安全的懶漢式單例:

public class SynchronizedLazyForm extends Form {

    private static SynchronizedLazyForm sInstance;

    // 私有化的構(gòu)造, 避免別人直接創(chuàng)建表格
    private SynchronizedLazyForm() {}

    // 店長(zhǎng)們通過(guò)這個(gè)接口來(lái)取表格
    // 注意, 這是一個(gè)synchronized方法
    // 參考https://docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html
    public static synchronized SynchronizedLazyForm getInstance() {

        // 在有店長(zhǎng)訪問(wèn)該文件時(shí)才創(chuàng)建, 通過(guò)判斷當(dāng)前文件是否存在(sInstance == null)來(lái)避免重復(fù)創(chuàng)建
        if (sInstance == null) {
            sInstance = new SynchronizedLazyForm();
        }
        return sInstance;
    }
}

線程安全的懶漢式單例
利用synchronized關(guān)鍵字來(lái)修飾對(duì)外提供該類唯一實(shí)例的接口(getInstance)來(lái)確保在一個(gè)線程調(diào)用該接口時(shí)能阻塞(block)另一個(gè)線程的調(diào)用, 從而達(dá)到多線程安全, 避免重復(fù)創(chuàng)建單例.

然而, synchronized有很大的性能開(kāi)銷. 而且在這里我們是修飾了getInstance方法, 意味著, 如果getInstance被很多線程頻繁調(diào)用時(shí), 每次都會(huì)做同步檢查, 會(huì)導(dǎo)致程序性能下降.

實(shí)際上我們要的是單例, 當(dāng)單例已經(jīng)存在的時(shí)候, 我們是不需要用同步方法來(lái)控制的. 一如我們第一種單例的實(shí)現(xiàn)---餓漢模式單例, 我們一開(kāi)始就創(chuàng)建好了單例, 就無(wú)需擔(dān)心線程同步問(wèn)題.

但是餓漢模式是提前創(chuàng)建, 那么我們?cè)趺茨茏龅窖舆t創(chuàng)建, 且線程安全, 且性能有所提升呢?

4, 雙重檢查鎖定DCL(Double-Checked Locking)單例

如上所言, 我們想要的是單例, 故而單例已經(jīng)存在的情況下我們無(wú)需做同步檢查, 如下實(shí)現(xiàn):

public class DCLForm extends Form {

    // 注意, 這里我們引入了volatile關(guān)鍵字
    private volatile static DCLForm sInstance;

    // 私有化的構(gòu)造, 避免別人直接創(chuàng)建表格
    private DCLForm() {}

    // 店長(zhǎng)們通過(guò)這個(gè)接口來(lái)取表格
    public static DCLForm getInstance() {

        // 第一次檢查
        if (sInstance == null) {
            // 第一次調(diào)用getInstance時(shí), sInstance為空, 進(jìn)入此分支
            // 使用synchronized block來(lái)確保多線程的安全
            synchronized (DCLForm.class) {
                // 第二次檢查
                if (sInstance == null) {
                    sInstance = new DCLForm();
                }
            }
        }
        return sInstance;
    }
}
  1. 舍棄了同步方法
  2. 在getInstance時(shí), 先檢查單例是否已經(jīng)存在, 如果存在了, 我們無(wú)需同步操作了, 任何線程過(guò)來(lái)直接取單例就行, 大大提升了性能.
  3. 若單例不存在(第一次調(diào)用時(shí)), 使用synchronized同步代碼塊, 來(lái)確保進(jìn)入的只有一個(gè)線程, 在此再做一次單例存在與否的檢查, 進(jìn)而創(chuàng)建出單例.

這樣就保證了:

  1. 在單例還沒(méi)有創(chuàng)建時(shí), 多個(gè)線程同時(shí)調(diào)用getInsance時(shí), 保證只有一個(gè)線程能夠執(zhí)行sInstance = new DCLForm()創(chuàng)建單例.
  2. 在單例已經(jīng)存在時(shí), getInsance沒(méi)有加鎖, 直接訪問(wèn), 訪問(wèn)創(chuàng)建好的單例, 從而達(dá)到性能提升.

注意
這里我們對(duì)sInstance使用的volatile關(guān)鍵字
具體原因和原理, 請(qǐng)參考這篇文章, 講的很詳細(xì).

然而, 使用volatile關(guān)鍵字的雙重檢查方案需要JDK5及以上(因?yàn)閺腏DK5開(kāi)始使用新的JSR-133內(nèi)存模型規(guī)范,這個(gè)規(guī)范增強(qiáng)了volatile的語(yǔ)義).

那么我們還有什么更通用的方式能保證多線程單例創(chuàng)建, 以及懶加載方式呢?

5, 靜態(tài)內(nèi)部類單例

public class StaticInnerClassForm extends Form {

    // 私有化的構(gòu)造, 避免別人直接創(chuàng)建表格
    private StaticInnerClassForm() {}
    
    // 店長(zhǎng)們通過(guò)這個(gè)接口來(lái)取表格
    public static StaticInnerClassForm getInstance() {
       return FormHolder.INSTANCE;
    }
    
    // 在靜態(tài)內(nèi)部類中實(shí)例化該單例
    private static class FormHolder {
       private static final StaticInnerClassForm INSTANCE = new StaticInnerClassForm();
    }
}

這種方式, 通過(guò)JVM的類加載方式(虛擬機(jī)會(huì)保證一個(gè)類的初始化在多線程環(huán)境中被正確的加鎖、同步), 來(lái)保證了多線程并發(fā)訪問(wèn)的正確性.

另外, 由于靜態(tài)內(nèi)部類的加載特性 --- 在使用時(shí)才加載, 這種方式也達(dá)成了懶加載的目的.

顯然, 這種方式是一種比較完美的單例模式. 當(dāng)然, 它也有其弊端, 依賴特定編程語(yǔ)言, 適用于JAVA平臺(tái).


還有很多單例的實(shí)現(xiàn)模式, 例如利用JDK 5起的Enum 枚舉單例模式, 使用容器類管理的單例模式等, 在此就不一一說(shuō)了, 網(wǎng)上都比較泛濫了...

從使用上, 如果是單線程環(huán)境的, 個(gè)人推薦使用第二種懶漢式單例, 簡(jiǎn)單便捷. 如果考慮多線程同步的話, 推薦使用第五種靜態(tài)內(nèi)部類單例, 確保同步且懶加載完美結(jié)合.


好了, 小光創(chuàng)建并改善了一套完整的日?qǐng)?bào)系統(tǒng). 這樣, 他每天就可以看到各個(gè)分店的戰(zhàn)況了, 也能根據(jù)各個(gè)店的問(wèn)題, 來(lái)及時(shí)協(xié)調(diào)資源解決, 保證各個(gè)分店的良好運(yùn)轉(zhuǎn)了.

最后編輯于
?著作權(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),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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