23天設計模式之單例模式

23天設計模式之單例模式

文章簡介

《23天設計模式之單例模式》這是我的第二篇部落格。在接下來的23天內,我們將23種設計模式都去了解一下。今天我們就來學習最簡單的單例模式

在學習設計模式之前我們不可避免要去想為什麼要學習這個東西,它是用來幹嘛的?

  • 要知道在軟體開發中,要實現可維護、可擴展,就必須盡量復用程式碼,並且降低程式碼的耦合度
  • 另外,我認為學習設計模式可以潛移默化地對我們的編程思想產生好的影響。其實我們經常都有使用到設計模式,只是沒有去學習過便沒有意識到,比如JDK的動態代理就是一種代理模式
  • 設計模式主要是基於OOP編程【面向對象編程(object-oriented programming);】提煉的,它基於以下幾個原則。

OOP7大原則

  • 開閉原則。對擴展開放,對修改關閉。
  • 里氏替換原則。繼承必須確保超類所擁有的性質在子類中仍然成立。(子類在程式中可以替換父類)
  • 依賴倒置原則。要面向介面編程,不要面向實現編程。(實現公共介面,而不是每個介面都實現)
  • 單一職責原則。控制類的粒度大小、將對象解耦、提高其內聚性。(方法的原子性)
  • 介面隔離原則。要為各個類建立他們所需要的專用介面。
  • 迪米特法則。只與你的直接朋友交談,不跟「陌生人」說話。
  • 合成復用原則。盡量先使用組合或聚合等關聯關係來實現,其次才考慮使用繼承關係來實現。

設計模式分類

總體分為3大類:

  • 創建型模式:關注點是如何創建對象,其核心思想是要把對象的創建和使用相分離,這樣使得兩者能相對獨立地變換。
    • 工廠方法:Factory Method
    • 抽象工廠:Abstract Factory
    • 建造者:Builder
    • 原型:Prototype
    • 單例:Singleton
  • 結構型模式。把類或對象結合在一起形成一個更大的結構。
    • 適配器
    • 橋接
    • 組合
    • 裝飾器
    • 外觀
    • 享元
    • 代理
  • 行為型模式。類和對象如何交互,及劃分責任和演算法。
    • 責任鏈
    • 命令
    • 解釋器
    • 迭代器
    • 中介
    • 備忘錄
    • 觀察者
    • 狀態
    • 策略
    • 模板方法
    • 訪問者

單例模式

  • 單例模式指在一個進程中,某個類有且僅有一個實例。
  • 為了防止調用者調用new方法繼續構造,構造方法要設置為private。
  • 調用者要怎麼獲取到單例呢?我們提供一個public static修飾的靜態方法給用戶返回單例。
  • 單例模式也分為以下幾種,在下面詳細介紹。

餓漢式單例

  • 所謂餓漢式單例是指,不管是否調用獲取實例的方法,都會產生一個單例。直接上程式碼!
  • 缺點:不管用不用都會產生單例,浪費空間。
  • 也稱為靜態單例模式。
public class Singleton {

    //靜態單例
    private static final Singleton INSTANCE = new Singleton();

    //私有的構造方法
    private Singleton(){};

    // 獲取單例的靜態方法
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

懶漢式單例

  • 所謂懶漢是指,這個單例很懶,只有在調用方調用時,才去生成單例。
  • 如果在多執行緒下不加鎖的話,會多次調用構造方法創建多個實例。直接上程式碼。
public class Singleton {

    //調用getInstance()方法時初始化
    private static Singleton instance;

    //輸出文字看這個構造方法被調用了幾次
    private Singleton(){
        System.out.println(Thread.currentThread().getName() + "調用了構造方法");
    };

    //加了判空
    public static Singleton getInstance(){
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }

    //使用多執行緒測試構造方法被調用了幾次。正常來說應該只調用一次。
    public static void main(String[] args) {
        for (int i = 0; i < 4; i++) {
            new Thread(Singleton::getInstance).start();
        }
    }
}

//輸出如下:
Thread-1調用了構造方法
Thread-2調用了構造方法
Thread-0調用了構造方法

分析:

  • 每次運行程式,輸出都不一樣;構造方法的明顯被多次調用,即創建了多個單例。

DCL懶漢式單例

  • DCL是指雙重檢鎖。double-checked locking
public class Singleton {

    //volatile禁止指令重排優化
    private static volatile Singleton instance;

    private Singleton(){
        System.out.println(Thread.currentThread().getName() + "調用了構造方法");
    };

    //DCL雙重檢鎖
    public static Singleton getInstance(){
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 4; i++) {
            new Thread(Singleton::getInstance).start();
        }
    }
}

//輸出:
Thread-0調用了構造方法

分析:

  • 無論執行多少次main函數,始終只有一條執行緒能調用構造函數。

  • 指令重排:編譯器、JVM 或者 CPU 都有可能出於優化等目的,對於實際指令執行的順序進行調整。

  • 為什麼要禁止指令重排:多執行緒環境中執行緒交替執行,由於編譯器指令重排的存在,兩個執行緒使用的變數能否保證一致性是無法確認的,結果無法預測。

靜態內部類實現單例

public class Singleton {

    private Singleton(){
    };

    public static Singleton getInstance() {
        return InnerClass.INSTANCE;
    }

    //靜態內部類
    public static class InnerClass {
        private static final Singleton INSTANCE = new Singleton();
    }
}

枚舉類實現單例

  • 正所謂道高一尺,魔高一丈。即使我們私有了構造器,單例也並不安全。萬能的反射可以仍調用私有構造器,因此我們需要枚舉單例
  • 枚舉單例不會被反射破壞。
public enum Singleton {

    INSTANCE;

    public Singleton getInstance(){
        return INSTANCE;
    }
}

以上就是單例模式的主要內容。接下來我們了解以下單例模式的常見應用場景。

單例模式的應用場景

  • Windows的Task Manager(任務管理器)就是很典型的單例模式

  • windows的Recycle Bin(回收站)就是典型的單例應用。在整個系統運行過程中,回收站一直維護著僅有的一個實例。

  • 網站的計數器,一般也是採用單例模式實現,否則難以同步。

  • 多執行緒的執行緒池的設計一般也是採用單例模式,這是由於執行緒池要方便對池中的執行緒進行控制。

以上

感謝您花時間閱讀我的部落格,以上就是我對單例模式的一些理解,若有不對之處,還望指正,期待與您交流。

本篇博文系原創,僅用於個人學習,轉載請註明出處。