跟我學設計模式之單例模式

1、設計模式

1.1 設計模式是什麼?

  1. 設計模式是解決特定問題的一系列套路,是前輩們的代碼設計經驗的總結,具備必定的廣泛性,能夠反覆使用。其目的是爲了提升代碼的可重用性、代碼的可讀性和代碼的可靠性。
  2. 設計模式的本質是面向對象設計原則的實際運用,是對類的封裝性、繼承性和多態性以及類的關聯關係和組合關係的充分理解。

1.2 爲何要使用設計模式?

項目的需求是永遠在變的,爲了應對這種變化,使得咱們的代碼可以輕易的實現解耦和拓展。java

1.3 設計模式類型

  • 建立型模式

建立型模式的主要關注點是怎樣建立對象,它的主要特色是將對象的建立與使用分離。這樣能夠下降系統的耦合度,使用者不須要關注對象的建立細節。git

  • 結構型模式

結構型模式描述如何將類或對象按某種佈局組成更大的結構。它分爲類結構型模式和對象結構型模式,前者採用繼承機制來組織接口和類,後者釆用組合或聚合來組合對象。github

  • 行爲型模式

行爲型模式用於描述程序在運行時複雜的流程控制,即描述多個類或對象之間怎樣相互協做共同完成單個對象都沒法單獨完成的任務,它涉及算法與對象間職責的分配。它分爲類行爲模式和對象行爲模式,前者採用繼承機制來在類間分派行爲,後者採用組合或聚合在對象間分配行爲。算法

建立型模式 結構型模式 行爲型模式
單例模式、抽象工廠模式、建造者模式、工廠模式、原型模式 適配器模式、橋接模式、裝飾模式、組合模式、外觀模式、享元模式、代理模式 模版方法模式、命令模式、迭代器模式、觀察者模式、中介者模式、備忘錄模式、解釋器模式、狀態模式、策略模式、職責鏈模式(責任鏈模式)、訪問者模式

2、面向對象設計的六大設計原則

2.1 開閉原則

一個軟件實體如類、模塊和函數應該對擴展開放,對修改關閉數據庫

  • 解讀
  1. 用抽象構建框架,用實現擴展細節;
  2. 不以改動原有類的方式來實現新需求,而是應該以實現事先抽象出來的接口(或具體類繼承抽象類)的方式來實現。
  • 優勢
  1. 能夠在不改動原有代碼的前提下給程序擴展功能,增長了程序的可擴展性;
  2. 同時也下降了程序的維護成本。

2.2 單一職責原則

一個類只容許有一個職責,即只有一個致使該類變動的緣由。編程

  • 解讀
  1. 類職責的變化每每就是致使類變化的緣由:也就是說若是一個類具備多種職責,就會有多種致使這個類變化的緣由,從而致使這個類的維護變得困難;設計模式

  2. 每每在軟件開發中隨着需求的不斷增長,可能會給原來的類添加一些原本不屬於它的一些職責,從而違反了單一職責原則。若是咱們發現當前類的職責不只僅有一個,就應該將原本不屬於該類真正的職責分離出去緩存

  3. 不只僅是類,函數(方法)也要遵循單一職責原則,即:一個函數(方法)只作一件事情。若是發現一個函數(方法)裏面有不一樣的任務,則須要將不一樣的任務以另外一個函數(方法)的形式分離出去。安全

  • 優勢
  1. 提升代碼的可讀性,更實際性地更下降了程序出錯的風險;
  2. 有利於bug的追蹤,下降了程序的維護成本。

2.3 依賴倒置原則

  1. 依賴抽象,而不是依賴實現;
  2. 抽象不該該依賴細節;細節應該依賴抽象;
  3. 高層模塊不能依賴低層模塊,兩者都應該依賴抽象。
  • 解讀
  1. 面向接口編程,而不是面向實現編程;
  2. 儘可能不要從具體的類派生,而是以繼承抽象類或實現接口來實現;
  3. 關於高層模塊與低層模塊的劃分能夠按照決策能力的高低進行劃分。業務層天然就處於上層模塊,邏輯層和數據層天然就歸類爲底層。
  • 優勢
  1. 經過抽象來搭建框架,創建類和類的關聯,以減小類間的耦合性;
  2. 以抽象搭建的系統要比以具體實現搭建的系統更加穩定,擴展性更高,同時也便於維護。
  • 里氏替換原則

子類能夠擴展父類的功能,但不能改變父類原有的功能。也就是說,子類繼承父類時,除添加新的方法完成新增功能外,儘可能不要重寫父類的方法。bash

2.4 接口隔離原則

多個特定的客戶端接口要好於一個通用性的總接口。

  • 解讀
  1. 客戶端不該該依賴它不須要實現的接口;
  2. 不創建龐大臃腫的接口,應儘可能細化接口,接口中的方法應該儘可能少。

注意:接口的粒度也不能過小。若是太小,則會形成接口數量過多,使設計複雜化。

  • 優勢

避免同一個接口裏麪包含不一樣類職責的方法,接口責任劃分更加明確,符合高內聚低耦合的思想。

2.5 迪米特法則(最少知道原則)

一個對象應該對儘量少的對象有接觸,也就是隻接觸那些真正須要接觸的對象。

  • 解讀

一個類應該只和它的成員變量,方法的輸入,返回參數中的類做交流,而不該該引入其餘的類(間接交流)。

  • 優勢

能夠良好地下降類與類之間的耦合,減小類與類之間的關聯程度,讓類與類之間的協做更加直接。

2.6 組合聚合複用原則

全部引用基類的地方必須能透明地使用其子類的對象,也就是說子類對象能夠替換其父類對象,而程序執行效果不變。

-解讀

在繼承體系中,子類中能夠增長本身特有的方法,也能夠實現父類的抽象方法,可是不能重寫父類的非抽象方法,不然該繼承關係就不是一個正確的繼承關係。

  • 優勢

能夠檢驗繼承使用的正確性,約束繼承在使用上的泛濫。

3、 單例模式概念

3.1 單例模式是什麼?

單例模式就是在程序運行中只實例化一次,建立一個全局惟一對象。有點像 Java 的靜態變量,可是單例模式要優於靜態變量:

  1. 靜態變量在程序啓動的時候JVM就會進行加載,若是不使用,會形成大量的資源浪費;
  2. 單例模式可以實現懶加載,可以在使用實例的時候纔去建立實例。

開發工具類庫中的不少工具類都應用了單例模式,比例線程池、緩存、日誌對象等,它們都只須要建立一個對象,若是建立多份實例,可能會帶來不可預知的問題,好比資源的浪費、結果處理不一致等問題。

3.2 爲何要使用單例模式?

單例模式屬於設計模式三大分類中的第一類——建立型模式,跟對象的建立相關。也就是說,這個模式在建立對象的同時,還致力於控制建立對象的數量,是的,只能建立一個實例,多的不要。

👉那麼問題來了,咱們爲何要控制對象建立的個數?

  1. 有些場景下,不使用單例模式,會致使系統同一時刻出現多個狀態缺少同步,用戶天然沒法判斷當前處於什麼狀態;
  2. 經過控制建立對象的數量,能夠節約系統資源開銷(像線程、數據庫鏈接等);
  3. 全局數據共享。

3.3 單例的實現思路

  1. 靜態化實例對象;
  2. 私有化構造方法,禁止經過構造方法建立實例;
  3. 提供一個公共的靜態方法,用來返回惟一實例。

4、 餓漢模式

在定義靜態屬性時,直接實例化了對象

4.1 示例

public class HungryMode {

    /** * 利用靜態變量來存儲惟一實例 */
    private static final HungryMode instance = new HungryMode();

    /** * 私有化構造函數 */
    private HungryMode(){
        // 裏面能夠有不少操做
    }

    /** * 提供公開獲取實例接口 * @return */
    public static HungryMode getInstance(){
        return instance;
    }
}
複製代碼

4.2 餓漢模式的優勢

因爲使用了static關鍵字,保證了在引用這個變量時,關於這個變量的因此寫入操做都完成,因此保證了JVM層面的線程安全。

4.3 餓漢模式的缺點

不能實現懶加載,形成空間浪費:若是一個類比較大,咱們在初始化的時就加載了這個類,可是咱們長時間沒有使用這個類,這就致使了內存空間的浪費。

因此,能不能只有用到 getInstance()方法,纔會去初始化單例類,纔會加載單例類中的數據。因此就有了:懶漢式

5、懶漢模式

懶漢模式是一種偷懶的模式,在程序初始化時不會建立實例,只有在使用實例的時候纔會建立實例,因此懶漢模式解決了餓漢模式帶來的空間浪費問題。

5.1 懶漢模式的通常實現

public class LazyMode {
    /** * 定義靜態變量時,未初始化實例 */
    private static LazyMode instance;

    /** * 私有化構造函數 */
    private LazyMode(){
        // 裏面能夠有不少操做
    }
    /** * 提供公開獲取實例接口 * @return */
    public static LazyMode getInstance(){
        // 使用時,先判斷實例是否爲空,若是實例爲空,則實例化對象
        if (instance == null) {
            instance = new LazyMode();
        }
        return instance;
    }
}
複製代碼

可是這種實如今多線程的狀況下是不安全的,有可能會出現多份實例的狀況:

if (instance == null) {
    instance = new LazyMode();
}
複製代碼

假設有兩個線程同時進入到上面這段代碼,由於沒有任何資源保護措施,因此兩個線程能夠同時判斷的 instance 都爲空,都將去初始化實例,因此就會出現多份實例的狀況。

5.2 懶漢模式的優化

咱們給getInstance()方法加上synchronized關鍵字,使得getInstance()方法成爲受保護的資源就可以解決多份實例的問題。

public class LazyModeSynchronized {
    /** * 定義靜態變量時,未初始化實例 */
    private static LazyModeSynchronized instance;
    /** * 私有化構造函數 */
    private LazyModeSynchronized(){
        // 裏面能夠有不少操做
    }
    /** * 提供公開獲取實例接口 * @return */
    public synchronized static LazyModeSynchronized getInstance(){
        /** * 添加class類鎖,影響了性能,加鎖以後將代碼進行了串行化, * 咱們的代碼塊絕大部分是讀操做,在讀操做的狀況下,代碼線程是安全的 * */
        if (instance == null) {
            instance = new LazyModeSynchronized();
        }
        return instance;
    }
}
複製代碼

5.3 懶漢模式的優勢

實現了懶加載,節約了內存空間。

5.4 懶漢模式的缺點

  1. 在不加鎖的狀況下,線程不安全,可能出現多份實例;
  2. 在加鎖的狀況下,會使程序串行化,使系統有嚴重的性能問題。

懶漢模式中加鎖的問題,對於getInstance()方法來講,絕大部分的操做都是讀操做,讀操做是線程安全的,因此咱們沒必讓每一個線程必須持有鎖才能調用該方法,咱們須要調整加鎖的問題。由此也產生了一種新的實現模式:雙重檢查鎖模式

6、雙重檢查鎖模式

6.1 雙重檢查鎖模式的通常實現

public class DoubleCheckLockMode {

    private static DoubleCheckLockMode instance;

    /** * 私有化構造函數 */
    private DoubleCheckLockMode(){

    }
    /** * 提供公開獲取實例接口 * @return */
    public static DoubleCheckLockMode getInstance(){
        // 第一次判斷,若是這裏爲空,不進入搶鎖階段,直接返回實例
        if (instance == null) {
            synchronized (DoubleCheckLockMode.class) {
                // 搶到鎖以後再次判斷是否爲空
                if (instance == null) {
                    instance = new DoubleCheckLockMode();
                }
            }
        }
        return instance;
    }
}
複製代碼

雙重檢查鎖模式解決了單例、性能、線程安全問題,可是這種寫法一樣存在問題:在多線程的狀況下,可能會出現空指針問題,出現問題的緣由是JVM在實例化對象的時候會進行優化和指令重排序操做。

6.2 什麼是指令重排?

private SingletonObject(){
	  // 第一步
     int x = 10;
	  // 第二步
     int y = 30;
     // 第三步
     Object o = new Object(); 
}
複製代碼

上面的構造函數SingletonObject()JVM 會對它進行指令重排序,因此執行順序可能會亂掉,可是無論是那種執行順序,JVM 最後都會保證因此實例都完成實例化。 若是構造函數中操做比較多時,爲了提高效率,JVM 會在構造函數裏面的屬性未所有完成實例化時,就返回對象。雙重檢測鎖出現空指針問題的緣由就是出如今這裏,當某個線程獲取鎖進行實例化時,其餘線程就直接獲取實例使用,因爲JVM指令重排序的緣由,其餘線程獲取的對象也許不是一個完整的對象,因此在使用實例的時候就會出現空指針異常問題

6.3 雙重檢查鎖模式優化

要解決雙重檢查鎖模式帶來空指針異常的問題,只須要使用volatile關鍵字,volatile關鍵字嚴格遵循happens-before原則,即:在讀操做前,寫操做必須所有完成。

public class DoubleCheckLockModelVolatile {
    /** * 添加volatile關鍵字,保證在讀操做前,寫操做必須所有完成 */
    private static volatile DoubleCheckLockModelVolatile instance;
    /** * 私有化構造函數 */
    private DoubleCheckLockModelVolatile(){

    }
    /** * 提供公開獲取實例接口 * @return */
    public static DoubleCheckLockModelVolatile getInstance(){

        if (instance == null) {
            synchronized (DoubleCheckLockModelVolatile.class) {
                if (instance == null) {
                    instance = new DoubleCheckLockModelVolatile();
                }
            }
        }
        return instance;
    }
}
複製代碼

7、靜態內部類模式

靜態內部類模式也稱單例持有者模式,實例由內部類建立,因爲 JVM 在加載外部類的過程當中, 是不會加載靜態內部類的, 只有內部類的屬性/方法被調用時纔會被加載, 並初始化其靜態屬性。靜態屬性由static修飾,保證只被實例化一次,而且嚴格保證明例化順序。

public class StaticInnerClassMode {

    private StaticInnerClassMode(){

    }

    /** * 單例持有者 */
    private static class InstanceHolder{
        private  final static StaticInnerClassMode instance = new StaticInnerClassMode();

    }

    /** * 提供公開獲取實例接口 * @return */
    public static StaticInnerClassMode getInstance(){
        // 調用內部類屬性
        return InstanceHolder.instance;
    }
}
複製代碼

這種方式跟餓漢式方式採用的機制相似,但又有不一樣。二者都是採用了類裝載的機制來保證初始化實例時只有一個線程。不一樣的地方:

  1. 餓漢式方式是隻要Singleton類被裝載就會實例化,沒有Lazy-Loading的做用;
  2. 靜態內部類方式在Singleton類被裝載時並不會當即實例化,而是在須要實例化時,調用getInstance()方法,纔會裝載SingletonInstance類,從而完成Singleton的實例化。

類的靜態屬性只會在第一次加載類的時候初始化,因此在這裏,JVM幫助咱們保證了線程的安全性,在類進行初始化時,別的線程是沒法進入的。

因此這種方式在沒有加任何鎖的狀況下,保證了多線程下的安全,而且沒有任何性能影響和空間的浪費

8、枚舉類實現單例模式

由於枚舉類型是線程安全的,而且只會裝載一次,設計者充分的利用了枚舉的這個特性來實現單例模式,枚舉的寫法很是簡單,並且枚舉類型是所用單例實現中惟一一種不會被破壞的單例實現模式

8.1 示例

public class EnumerationMode {
    
    private EnumerationMode(){
        
    }

    /** * 枚舉類型是線程安全的,而且只會裝載一次 */
    private enum Singleton{
        INSTANCE;

        private final EnumerationMode instance;

        Singleton(){
            instance = new EnumerationMode();
        }

        private EnumerationMode getInstance(){
            return instance;
        }
    }

    public static EnumerationMode getInstance(){

        return Singleton.INSTANCE.getInstance();
    }
}
複製代碼

8.2 適用場合:

  1. 須要頻繁的進行建立和銷燬的對象;
  2. 建立對象時耗時過多或耗費資源過多,但又常常用到的對象;
  3. 工具類對象;
  4. 頻繁訪問數據庫或文件的對象。

9、單例模式的問題及解決辦法

除枚舉方式外, 其餘方法都會經過反射的方式破壞單例

9.1 單例模式的破壞

/** * 以靜態內部類實現爲例 * @throws Exception */
@Test
public void singletonTest() throws Exception {
    Constructor constructor = StaticInnerClassMode.class.getDeclaredConstructor();
    constructor.setAccessible(true);

    StaticInnerClassMode obj1 = StaticInnerClassMode.getInstance();
    StaticInnerClassMode obj2 = StaticInnerClassMode.getInstance();
    StaticInnerClassMode obj3 = (StaticInnerClassMode) constructor.newInstance();

    System.out.println("輸出結果爲:"+obj1.hashCode()+"," +obj2.hashCode()+","+obj3.hashCode());
}
複製代碼

控制檯打印:

輸出結果爲:1454171136,1454171136,1195396074
複製代碼

從輸出的結果咱們就能夠看出obj1obj2爲同一對象,obj3爲新對象。obj3是咱們經過反射機制,進而調用了私有的構造函數,而後產生了一個新的對象。

9.2 如何阻止單例破壞

能夠在構造方法中進行判斷,若已有實例, 則阻止生成新的實例,解決辦法以下:

public class StaticInnerClassModeProtection {

    private static boolean flag = false;

    private StaticInnerClassModeProtection(){
        synchronized(StaticInnerClassModeProtection.class){
            if(flag == false){
                flag = true;
            }else {
                throw new RuntimeException("實例已經存在,請經過 getInstance()方法獲取!");
            }
        }
    }

    /** * 單例持有者 */
    private static class InstanceHolder{
        private  final static StaticInnerClassModeProtection instance = new StaticInnerClassModeProtection();
    }

    /** * 提供公開獲取實例接口 * @return */
    public static StaticInnerClassModeProtection getInstance(){
        // 調用內部類屬性
        return InstanceHolder.instance;
    }
}
複製代碼

測試:

/** * 在構造方法中進行判斷,若存在則拋出RuntimeException * @throws Exception */
@Test
public void destroyTest() throws Exception {
    Constructor constructor = StaticInnerClassModeProtection.class.getDeclaredConstructor();
    constructor.setAccessible(true);

    StaticInnerClassModeProtection obj1 = StaticInnerClassModeProtection.getInstance();
    StaticInnerClassModeProtection obj2 = StaticInnerClassModeProtection.getInstance();
    StaticInnerClassModeProtection obj3 = (StaticInnerClassModeProtection) constructor.newInstance();

    System.out.println("輸出結果爲:"+obj1.hashCode()+"," +obj2.hashCode()+","+obj3.hashCode());
}
複製代碼

控制檯打印:

Caused by: java.lang.RuntimeException: 實例已經存在,請經過 getInstance()方法獲取!
	at cn.van.singleton.demo.mode.StaticInnerClassModeProtection.<init>(StaticInnerClassModeProtection.java:22)
	... 35 more
複製代碼

10、總結

10.1 各類實現的對比

名稱 餓漢模式 懶漢模式 雙重檢查鎖模式 靜態內部類實現 枚舉類實現
可用性 可用 不推薦使用 推薦使用 推薦使用 推薦使用
特色 不能實現懶加載,可能形成空間浪費 不加鎖線程不安全;加鎖性能差 線程安全;延遲加載;效率較高 避免了線程不安全,延遲加載,效率高。 寫法簡單;線程安全;只裝載一次

10.2 示例代碼地址

Github 示例代碼

10.3 技術交流

  1. 風塵博客:https://www.dustyblog.cn
  2. 風塵博客-掘金
  3. 風塵博客-博客園
  4. Github
  5. 公衆號
    風塵博客

10.4 參考文章

面向對象設計的六大設計原則

相關文章
相關標籤/搜索