爲何用枚舉類來實現單例模式愈來愈流行?

前言

單例模式是 Java 設計模式中最簡單的一種,只須要一個類就能實現單例模式,可是,你可不能小看單例模式,雖然從設計上來講它比較簡單,可是在實現當中你會遇到很是多的坑,因此,繫好安全帶,上車。java

單例模式的定義

單例模式就是在程序運行中只實例化一次,建立一個全局惟一對象,有點像 Java 的靜態變量,可是單例模式要優於靜態變量,靜態變量在程序啓動的時候JVM就會進行加載,若是不使用,會形成大量的資源浪費,單例模式可以實現懶加載,可以在使用實例的時候纔去建立實例。開發工具類庫中的不少工具類都應用了單例模式,比例線程池、緩存、日誌對象等,它們都只須要建立一個對象,若是建立多份實例,可能會帶來不可預知的問題,好比資源的浪費、結果處理不一致等問題。設計模式

單例的實現思路

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

單例的好處

  • 只有一個對象,內存開支少、性能好緩存

  • 避免對資源的多重佔用安全

  • 在系統設置全局訪問點,優化和共享資源訪問微信

單例模式的實現

單例模式的寫法有餓漢模式、懶漢模式、雙重檢查鎖模式、靜態內部類單例模式、枚舉類實現單例模式五種方式,其中懶漢模式、雙重檢查鎖模式兩種方式,若是你寫法不當,在多線程狀況下會存在不是單例或者單例出異常等問題,具體的緣由,在後面的對應處會進行說明。咱們從最基本的餓漢模式開始咱們的單例編寫之路。多線程

餓漢模式

餓漢模式採用一種簡單粗暴的形式,在定義靜態屬性時,直接實例化了對象。代碼以下:app

//在類加載時就完成了初始化,因此類加載較慢,但獲取對象的速度快
public class SingletonObject1 {
    // 利用靜態變量來存儲惟一實例
    private static final SingletonObject1 instance = new SingletonObject1();

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

    // 提供公開獲取實例接口
    public static SingletonObject1 getInstance(){
        return instance;
    }
}

複製代碼

餓漢模式的優缺點

優勢
  • 因爲使用了static關鍵字,保證了在引用這個變量時,關於這個變量的因此寫入操做都完成,因此保證了JVM層面的線程安全
缺點
  • 不能實現懶加載,形成空間浪費,若是一個類比較大,咱們在初始化的時就加載了這個類,可是咱們長時間沒有使用這個類,這就致使了內存空間的浪費。

懶漢模式

懶漢模式是一種偷懶的模式,在程序初始化時不會建立實例,只有在使用實例的時候纔會建立實例,因此懶漢模式解決了餓漢模式帶來的空間浪費問題,同時也引入了其餘的問題,咱們先來看看下面這個懶漢模式函數

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

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

    }

    public static SingletonObject2 getInstance(){
        // 使用時,先判斷實例是否爲空,若是實例爲空,則實例化對象
        if (instance == null)
            instance = new SingletonObject2();
        return instance;
    }
}
複製代碼

上面是懶漢模式的實現方式,可是上面這段代碼在多線程的狀況下是不安全的,由於它不能保證是單例模式,有可能會出現多份實例的狀況,出現多份實例的狀況是在建立實例對象時候形成的。因此我單獨把實例化的代碼提出,來分析一下爲何會出現多份實例的狀況。工具

1   if (instance == null)
     2       instance = new SingletonObject2();
複製代碼

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

經過上面的分析咱們已經知道出現多份實例的緣由,若是咱們在建立實例的時候進行資源保護,是否是能夠解決多份實例的問題?確實如此,咱們給getInstance()方法加上synchronized關鍵字,使得getInstance()方法成爲受保護的資源就可以解決多份實例的問題。加上synchronized關鍵字以後代碼以下:

public class SingletonObject3 {
    private static SingletonObject3 instance;

    private SingletonObject3(){

    }

    public synchronized static SingletonObject3 getInstance(){
        /** * 添加class類鎖,影響了性能,加鎖以後將代碼進行了串行化, * 咱們的代碼塊絕大部分是讀操做,在讀操做的狀況下,代碼線程是安全的 * */

        if (instance == null)
            instance = new SingletonObject3();
        return instance;
    }
}
複製代碼

通過修改後,咱們解決了多份實例的問題,可是由於加入了synchronized關鍵字,對代碼加了鎖,就引入了新的問題,加鎖以後會使得程序變成串行化,只有搶到鎖的線程才能去執行這段代碼塊,這會使得系統的性能大大降低。

懶漢模式的優缺點

優勢
  • 實現了懶加載,節約了內存空間
缺點
  • 在不加鎖的狀況下,線程不安全,可能出現多份實例
  • 在加鎖的狀況下,會是程序串行化,使系統有嚴重的性能問題

雙重檢查鎖模式

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

public class SingletonObject4 {
    private static SingletonObject4 instance;

    private SingletonObject4(){

    }

    public static SingletonObject4 getInstance(){

        // 第一次判斷,若是這裏爲空,不進入搶鎖階段,直接返回實例
        if (instance == null)
            synchronized (SingletonObject4.class){
                // 搶到鎖以後再次判斷是否爲空
                if (instance == null){
                    instance = new SingletonObject4();
                }
            }

        return instance;
    }
}
複製代碼

雙重檢查鎖模式是一種很是好的單例實現模式,解決了單例、性能、線程安全問題,上面的雙重檢測鎖模式看上去天衣無縫,實際上是存在問題,在多線程的狀況下,可能會出現空指針問題,出現問題的緣由是JVM在實例化對象的時候會進行優化和指令重排序操做。什麼是指令重排?,看下面這個例子,簡單瞭解一下指令從排序

private SingletonObject4(){
     1   int x = 10;
     2   int y = 30;
     3  Object o = new Object();
                
    }
複製代碼

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

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

// 添加volatile關鍵字
    private static volatile SingletonObject5 instance;

    private SingletonObject5(){

    }

    public static SingletonObject5 getInstance(){

        if (instance == null)
            synchronized (SingletonObject5.class){
                if (instance == null){
                    instance = new SingletonObject5();
                }
            }

        return instance;
    }
}
複製代碼

添加volatile關鍵字以後的雙重檢查鎖模式是一種比較好的單例實現模式,可以保證在多線程的狀況下線程安全也不會有性能問題。

靜態內部類單例模式

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

public class SingletonObject6 {


    private SingletonObject6(){

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

    }
    
    // 
    public static SingletonObject6 getInstance(){
        // 調用內部類屬性
        return InstanceHolder.instance;
    }
}
複製代碼

靜態內部類單例模式是一種優秀的單例模式,是開源項目中比較經常使用的一種單例模式。在沒有加任何鎖的狀況下,保證了多線程下的安全,而且沒有任何性能影響和空間的浪費。

枚舉類實現單例模式

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

public class SingletonObject7 {


    private SingletonObject7(){

    }

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

        private final SingletonObject7 instance;

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

        private SingletonObject7 getInstance(){
            return instance;
        }
    }

    public static SingletonObject7 getInstance(){

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

破壞單例模式的方法及解決辦法

一、除枚舉方式外, 其餘方法都會經過反射的方式破壞單例,反射是經過調用構造方法生成新的對象,因此若是咱們想要阻止單例破壞,能夠在構造方法中進行判斷,若已有實例, 則阻止生成新的實例,解決辦法以下:

private SingletonObject1(){
    if (instance !=null){
        throw new RuntimeException("實例已經存在,請經過 getInstance()方法獲取");
    }
}
複製代碼

二、若是單例類實現了序列化接口Serializable, 就能夠經過反序列化破壞單例,因此咱們能夠不實現序列化接口,若是非得實現序列化接口,能夠重寫反序列化方法readResolve(), 反序列化時直接返回相關單例對象。

public Object readResolve() throws ObjectStreamException {
        return instance;
    }
複製代碼

若是你以爲文章不錯,歡迎點贊轉發

最後

打個小廣告,歡迎掃碼關注微信公衆號:「平頭哥的技術博文」,一塊兒進步吧。

平頭哥的技術博文
相關文章
相關標籤/搜索