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

一、創建型模式

創建型模式的主要關注點是“怎樣創建對象?”,它的主要特點是“將對象的創建與使用分離”。

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

創建型模式分爲:

  • 單例模式

  • 工廠方法模式

  • 抽象工廠模式

  • 原型模式

  • 建造者模式

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;
    }

    說明:

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

    未完待續。。。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章