日報表格只有一份---單例模式

前情提要

上集講到, 小光創建了開分店的標準(工廠), 之後開分店都按照這套標準執行(從CompanyFactory的實現中生產開分店的必須東西), 開分店變得更加容易了.javascript

小光也是立刻將本身的這套"開分公司的工廠"投入使用了, 開出了花山軟件新城分店.html

隨着分店愈來愈多, 小光也請了分別請了店長來"代理"小光以前的職責. 固然, 小光可不能徹底聽任無論啊, 他想着我至少得知道下天天各個店的基本狀況吧.java

全部示例源碼已經上傳到Github, 戳這裏git

日報制度

小光想到了當時作程序猿時, 敏捷開發天天站立會議的三個問題:github

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

心想, 我也能夠根據這個弄個日報制度啊, 讓各個店長按照這個格式彙報下當天的戰績:
編程

表格:安全

public class Form {

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

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

    @Override
    public String toString() {
        return "表格:" + this.hashCode() + ", 數據:" + mFormData;
    }
}複製代碼

天天讓各個店子在打烊以前在系統中拿當日的表格(若是尚未, 就建立一個)來填寫數據, 而後提交.多線程

出了問題

想法挺好, 可是剛剛用上, 就出了問題:併發

光谷店店長表妹登陸系統, 發現2016-12-16這個文件夾中尚未表格文件, 因而本地建立了一個, 用來填寫數據, 準備稍後提交. 然而此時, 花山店的店長小章也登陸了系統, 也發現尚未表格文件, 也建立了一個...oracle

讓咱們來看下操做:

表妹:

public class Cousins {

    public Form submitReport() {
        Form form = new Form();
        form.write("光谷店數據");
        return form;
    }
}複製代碼

小章:

public class XiaoZhang {

    public Form submitReport() {
        Form form = new Form();
        form.write("花山店數據");
        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);
    }
}複製代碼

來看下結果:

表格:1639705018, 數據:[光谷店數據]
表格:1627674070, 數據:[花山店數據]複製代碼

最終這個文件夾中有了兩個(不一樣的)表格, 小光看起來非常不方便...

表格應該只能有一份

表格只能有一份, 小光心想. 那麼怎麼保證呢, 很簡單, 我提早給建立好, 你們經過統一的接口來取這個文件, 而不能本身建立. 這樣就不會有問題了:

public class HungryForm extends Form {

    // 提早建立好
    private static HungryForm sInstance = new HungryForm();

    // 私有化的構造, 避免別人直接建立表格
    private HungryForm() {}

    // 店長們經過這個接口來取表格
    public static HungryForm getInstance() {
        return sInstance;
    }
}複製代碼

店長們這樣提交報告:

public class Cousins {

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

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

public class XiaoZhang {

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

        // 從固定的接口取表格
        Form form = HungryForm.getInstance();
        form.write("花山店數據");
        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);
    }
}複製代碼

來看下如今的結果:

表格:1639705018, 數據:[光谷店數據]
表格:1639705018, 數據:[光谷店數據, 花山店數據]複製代碼

能夠看到兩人用的是同一份表格(hashCode同樣的), 生成的數據也沒有問題了.

故事以後

看到這, 同窗們應該都看出來了, 小光這就是使用了大名鼎鼎的單例模式.
照例, 看下類圖, 這個應該是最簡單的類圖了:

單例模式
保證一個類(HungryForm)僅有一個實例(sInstance), 並提供一個訪問該實例的全局訪問點(getInstance).
這就意味着單例一般有以下兩個特色:

  1. 構造函數是私有的(避免別的地方建立它)
  2. 有一個static的方法來對外提供一個該單例的實例.

擴展閱讀一

同窗們可能注意到了, 咱們在這個單例模式中使用了Hungry這個詞, 沒錯, 咱們這裏實現單例的方式使用的就是餓漢式.

1, 餓漢式單例

餓漢式單例
顧名思義, 就是很餓, 無論三七二十一先建立了一個實例放着, 而無論最終用不用.

然而, 這個單例可能最終並不須要, 若是提早就建立好, 就會浪費內存空間了.
例如, 咱們這個故事中, 年末假期中, 全部店子都歇業十天, 這十天就沒有任何店長會去訪問這個表格, 然而小光仍是都天天都建立了, 這就形成了空間浪費(假設這個表格數據(對象實例)很大...)

2, 懶漢式單例

那麼怎麼辦呢?
咱們可使用懶漢式單例:

public class LazyForm extends Form {

    private static LazyForm sInstance;

    // 私有化的構造, 避免別人直接建立表格
    private LazyForm() {}

    // 店長們經過這個接口來取表格
    public static LazyForm getInstance() {

        // 在有店長訪問該文件時才建立, 經過判斷當前文件是否存在(sInstance == null)來避免重複建立
        if (sInstance == null) {
            sInstance = new LazyForm();
        }
        return sInstance;
    }
}複製代碼

懶漢式單例
"懶", 也就是如今懶得建立, 等有用戶要用的時候才建立.

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

可是這樣建立也會有問題啊, 由於他是經過sInstance == null判斷當前是否已經存在表格文件的, 假設有兩個店長同時調用getInstance來取文件, 同時走到sInstance == null判斷這一步, 就會出問題了 --- 有可能建立了兩個文件(實例), 就達不到單例的目的了.

因此說這種懶漢式是線程不安全的, 在多線程環境下, 並不能作到單例.

那麼, 該如何作, 既能懶加載, 又線程安全呢?
咱們都知道Java中多線程環境每每會用到synchronized關鍵字, 經過他來作線程併發性控制.

synchronized方法控制對類成員變量的訪問, 每一個類實例對應一把鎖, synchronized修飾的方法必須得到調用該方法的類實例的鎖方能執行, 不然所屬線程阻塞. 方法一旦執行, 就獨佔該鎖. 直到從該方法返回時纔將鎖釋放. 此後被阻塞的線程方能得到該鎖, 從新進入可執行狀態.

讓咱們來看下線程安全的懶漢式單例:

public class SynchronizedLazyForm extends Form {

    private static SynchronizedLazyForm sInstance;

    // 私有化的構造, 避免別人直接建立表格
    private SynchronizedLazyForm() {}

    // 店長們經過這個接口來取表格
    // 注意, 這是一個synchronized方法
    // 參考https://docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html
    public static synchronized SynchronizedLazyForm getInstance() {

        // 在有店長訪問該文件時才建立, 經過判斷當前文件是否存在(sInstance == null)來避免重複建立
        if (sInstance == null) {
            sInstance = new SynchronizedLazyForm();
        }
        return sInstance;
    }
}複製代碼

線程安全的懶漢式單例
利用synchronized關鍵字來修飾對外提供該類惟一實例的接口(getInstance)來確保在一個線程調用該接口時能阻塞(block)另外一個線程的調用, 從而達到多線程安全, 避免重複建立單例.

然而, synchronized有很大的性能開銷. 並且在這裏咱們是修飾了getInstance方法, 意味着, 若是getInstance被不少線程頻繁調用時, 每次都會作同步檢查, 會致使程序性能降低.

實際上咱們要的是單例, 當單例已經存在的時候, 咱們是不須要用同步方法來控制的. 一如咱們第一種單例的實現---餓漢模式單例, 咱們一開始就建立好了單例, 就無需擔憂線程同步問題.

可是餓漢模式是提早建立, 那麼咱們怎麼能作到延遲建立, 且線程安全, 且性能有所提高呢?

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

如上所言, 咱們想要的是單例, 故而單例已經存在的狀況下咱們無需作同步檢查, 以下實現:

public class DCLForm extends Form {

    // 注意, 這裏咱們引入了volatile關鍵字
    private volatile static DCLForm sInstance;

    // 私有化的構造, 避免別人直接建立表格
    private DCLForm() {}

    // 店長們經過這個接口來取表格
    public static DCLForm getInstance() {

        // 第一次檢查
        if (sInstance == null) {
            // 第一次調用getInstance時, sInstance爲空, 進入此分支
            // 使用synchronized block來確保多線程的安全
            synchronized (DCLForm.class) {
                // 第二次檢查
                if (sInstance == null) {
                    sInstance = new DCLForm();
                }
            }
        }
        return sInstance;
    }
}複製代碼
  1. 捨棄了同步方法
  2. 在getInstance時, 先檢查單例是否已經存在, 若是存在了, 咱們無需同步操做了, 任何線程過來直接取單例就行, 大大提高了性能.
  3. 若單例不存在(第一次調用時), 使用synchronized同步代碼塊, 來確保進入的只有一個線程, 在此再作一次單例存在與否的檢查, 進而建立出單例.

這樣就保證了:

  1. 在單例尚未建立時, 多個線程同時調用getInsance時, 保證只有一個線程可以執行sInstance = new DCLForm()建立單例.
  2. 在單例已經存在時, getInsance沒有加鎖, 直接訪問, 訪問建立好的單例, 從而達到性能提高.

注意
這裏咱們對sInstance使用的volatile關鍵字
具體緣由和原理, 請參考這篇文章, 講的很詳細.

然而, 使用volatile關鍵字的雙重檢查方案須要JDK5及以上(由於從JDK5開始使用新的JSR-133內存模型規範,這個規範加強了volatile的語義).

那麼咱們還有什麼更通用的方式能保證多線程單例建立, 以及懶加載方式呢?

5, 靜態內部類單例

public class StaticInnerClassForm extends Form {

    // 私有化的構造, 避免別人直接建立表格
    private StaticInnerClassForm() {}

    // 店長們經過這個接口來取表格
    public static StaticInnerClassForm getInstance() {
       return FormHolder.INSTANCE;
    }

    // 在靜態內部類中實例化該單例
    private static class FormHolder {
       private static final StaticInnerClassForm INSTANCE = new StaticInnerClassForm();
    }
}複製代碼

這種方式, 經過JVM的類加載方式(虛擬機會保證一個類的初始化在多線程環境中被正確的加鎖、同步), 來保證了多線程併發訪問的正確性.

另外, 因爲靜態內部類的加載特性 --- 在使用時才加載, 這種方式也達成了懶加載的目的.

顯然, 這種方式是一種比較完美的單例模式. 固然, 它也有其弊端, 依賴特定編程語言, 適用於JAVA平臺.


還有不少單例的實現模式, 例如利用JDK 5起的Enum 枚舉單例模式, 使用容器類管理的單例模式等, 在此就不一一說了, 網上都比較氾濫了...

從使用上, 若是是單線程環境的, 我的推薦使用第二種懶漢式單例, 簡單便捷. 若是考慮多線程同步的話, 推薦使用第五種靜態內部類單例, 確保同步且懶加載完美結合.


好了, 小光建立並改善了一套完整的日報系統. 這樣, 他天天就能夠看到各個分店的戰況了, 也能根據各個店的問題, 來及時協調資源解決, 保證各個分店的良好運轉了.

相關文章
相關標籤/搜索