設計模式(三)----建立型模式之單例模式(一)

2023-02-02 21:00:25

一、建立型模式

建立型模式的主要關注點是「怎樣建立物件?」,它的主要特點是「將物件的建立與使用分離」。

這樣可以降低系統的耦合度,使用者不需要關注物件的建立細節。

建立型模式分為:

  • 單例模式

  • 工廠方法模式

  • 抽象工廠模式

  • 原型模式

  • 建造者模式

1.1 單例設計模式

單例模式(Singleton Pattern)是 Java 中最簡單的設計模式之一。這種型別的設計模式屬於建立型模式,它提供了一種建立物件的最佳方式。

這種模式涉及到一個單一的類,該類負責建立自己的物件,同時確保只有單個物件被建立。這個類提供了一種存取其唯一的物件的方式,可以直接存取,不需要範例化該類的物件。

1.1.1 單例模式的結構

單例模式的主要有以下角色:

  • 單例類。只能建立一個範例的類

  • 存取類(測試類)。使用單例類

1.1.2 單例模式的實現

單例設計模式分類兩種:

餓漢式:類載入就會導致該單範例物件被建立

懶漢式:類載入不會導致該單範例物件被建立,而是首次使用該物件時才會建立

  1. 餓漢式-方式1(靜態變數方式)

    /**
     * 餓漢式
     *      靜態變數建立類的物件
     */
    public class Singleton {
        //私有構造方法
        private Singleton() {}
    ​
        //在成員位置建立該類的物件
        private static Singleton instance = new Singleton();
    ​
        //對外提供靜態方法獲取該物件
        public static Singleton getInstance() {
            return instance;
        }
    }

    說明:

    該方式在成員位置宣告Singleton型別的靜態變數,並建立Singleton類的物件instance。instance物件是隨著類的載入而建立的。如果該物件足夠大的話,而一直沒有使用就會造成記憶體的浪費。

    用下面的程式碼來驗證一下

    public class Client {
        public static void main(String[] args) {
            //建立Singleton類的物件
            Singleton instance = Singleton.getInstance();
    ​
            Singleton instance1 = Singleton.getInstance();
    ​
            //判斷獲取到的兩個是否是同一個物件
            System.out.println(instance == instance1);
        }
    }

    可以得出單例模式得到的物件是一模一樣的。

  2. 餓漢式-方式2(靜態程式碼塊方式)

    /**
     * 餓漢式
     *      在靜態程式碼塊中建立該類物件
     */
    public class Singleton {
    ​
        //私有構造方法
        private Singleton() {}
    ​
        //在成員位置建立該類的物件
        private static Singleton instance;
    ​
        static {
            instance = new Singleton();
        }
    ​
        //對外提供靜態方法獲取該物件
        public static Singleton getInstance() {
            return instance;
        }
    }

    說明:

    該方式在成員位置宣告Singleton型別的靜態變數,而物件的建立是在靜態程式碼塊中,也是對著類的載入而建立。所以和餓漢式的方式1基本上一樣,當然該方式也存在記憶體浪費問題。

    驗證方式同上可得出相同結論。

  3. 懶漢式-方式1(執行緒不安全)

    /**
     * 懶漢式
     *  執行緒不安全
     */
    public class Singleton {
        //私有構造方法
        private Singleton() {}
    ​
        //在成員位置建立該類的物件
        private static Singleton instance;
    ​
        //對外提供靜態方法獲取該物件
        public static Singleton getInstance() {
            //判斷instance是否為null,如果為null,說明還沒有建立Singleton類的物件
            //如果沒有,建立一個並返回,如果有,直接返回
            if(instance == null) {
                //執行緒1等待,執行緒2獲取到cpu的執行權,也會進入到該判斷裡面
                instance = new Singleton();
            }
            return instance;
        }
    }

    說明:

    從上面程式碼我們可以看出該方式在成員位置宣告Singleton型別的靜態變數,並沒有進行物件的賦值操作,那麼什麼時候賦值的呢?當呼叫getInstance()方法獲取Singleton類的物件的時候才建立Singleton類的物件,這樣就實現了懶載入的效果。但是,如果是多執行緒環境,會出現執行緒安全問題。

  4. 懶漢式-方式2(執行緒安全)

    /**
     * 懶漢式
     *  執行緒安全
     */
    public class Singleton {
        //私有構造方法
        private Singleton() {}
    ​
        //在成員位置建立該類的物件
        private static Singleton instance;
    ​
        //對外提供靜態方法獲取該物件
        public static synchronized Singleton getInstance() {
    ​
            if(instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
    }

    說明:

    該方式也實現了懶載入效果,同時又解決了執行緒安全問題。但是在getInstance()方法上新增了synchronized關鍵字,導致該方法的執行效果特別低。從上面程式碼我們可以看出,其實就是在初始化instance的時候才會出現執行緒安全問題,一旦初始化完成就不存在了。

  5. 懶漢式-方式3(雙重檢查鎖)

    再來討論一下懶漢模式中加鎖的問題,對於 getInstance() 方法來說,絕大部分的操作都是讀操作,讀操作是執行緒安全的,所以我們沒必讓每個執行緒必須持有鎖才能呼叫該方法,我們需要調整加鎖的時機。由此也產生了一種新的實現模式:雙重檢查鎖模式

    /**
     * 雙重檢查方式
     */
    public class Singleton { 
    ​
        //私有構造方法
        private Singleton() {}
    ​
        private static Singleton instance;
    ​
       //對外提供靜態方法獲取該物件
        public static Singleton getInstance() {
            //第一次判斷,如果instance不為null,不進入搶鎖階段,直接返回範例
            if(instance == null) {
                synchronized (Singleton.class) {
                    //搶到鎖之後再次判斷是否為null
                    if(instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }

    雙重檢查鎖模式是一種非常好的單例實現模式,解決了單例、效能、執行緒安全問題,上面的雙重檢測鎖模式看上去完美無缺,其實是存在問題,在多執行緒的情況下,可能會出現空指標問題,出現問題的原因是JVM在範例化物件的時候會進行優化和指令重排序操作。

    要解決雙重檢查鎖模式帶來空指標異常的問題,只需要使用 volatile 關鍵字, volatile 關鍵字可以保證可見性和有序性。

    /**
     * 雙重檢查方式
     */
    public class Singleton {
    ​
        //私有構造方法
        private Singleton() {}
    ​
        private static volatile Singleton instance;
    ​
       //對外提供靜態方法獲取該物件
        public static Singleton getInstance() {
            //第一次判斷,如果instance不為null,不進入搶鎖階段,直接返回實際
            if(instance == null) {
                synchronized (Singleton.class) {
                    //搶到鎖之後再次判斷是否為空
                    if(instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }

    小結:

    新增 volatile 關鍵字之後的雙重檢查鎖模式是一種比較好的單例實現模式,能夠保證在多執行緒的情況下執行緒安全也不會有效能問題。

  6. 懶漢式-方式4(靜態內部類方式)

    靜態內部類單例模式中範例由內部類建立,由於 JVM 在載入外部類的過程中, 是不會載入靜態內部類的, 只有內部類的屬性/方法被呼叫時才會被載入, 並初始化其靜態屬性。靜態屬性由於被 static 修飾,保證只被範例化一次,並且嚴格保證範例化順序。

    /**
     * 靜態內部類方式
     */
    public class Singleton {
    ​
        //私有構造方法
        private Singleton() {}
    ​
        //定義一個靜態內部類
        private static class SingletonHolder {
            //在內部類中宣告並初始化外部類的對
            private static final Singleton INSTANCE = new Singleton();
        }
    ​
        //對外提供靜態方法獲取該物件
        public static Singleton getInstance() {
            return SingletonHolder.INSTANCE;
        }
    }

    驗證是否正確的方式同上。

    說明:

    第一次載入Singleton類時不會去初始化INSTANCE,只有第一次呼叫getInstance,虛擬機器器載入SingletonHolder

    並初始化INSTANCE,這樣不僅能確保執行緒安全,也能保證 Singleton 類的唯一性。

    小結:

    靜態內部類單例模式是一種優秀的單例模式,是開源專案中比較常用的一種單例模式。在沒有加任何鎖的情況下,保證了多執行緒下的安全,並且沒有任何效能影響和空間的浪費。

  7. 列舉方式

    列舉類實現單例模式是極力推薦的單例實現模式,因為列舉型別是執行緒安全的,並且只會裝載一次,設計者充分的利用了列舉的這個特性來實現單例模式,列舉的寫法非常簡單,而且列舉型別是所用單例實現中唯一一種不會被破壞的單例實現模式。

    /**
     * 列舉方式
     */
    public enum Singleton {
        INSTANCE;
    }

    說明:

    列舉方式屬於餓漢式方式。

    未完待續。。。