使用枚舉來寫出更優雅的單例設計模式

Java 中的單例設計模式,不少時候咱們只會注意到線程引發的表象性問題,可是沒考慮過對反射機制的限制,此文旨在簡單介紹利用枚舉來防止反射的漏洞。java

1、最多見的單例

咱們先展現一段最多見的懶漢式的單例:設計模式

public class Singleton {

    private Singleton(){} // 私有構造

    private static Singleton instance = null// 私有單例對象

    // 靜態工廠
    public static Singleton getInstance(){
        if (instance == null) { // 雙重檢測機制
            synchronized (Singleton.class) { // 同步鎖
                if (instance == null) { // 雙重檢測機制
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

}
複製代碼

上述單例的寫法採用的雙重檢查機制增長了必定的安全性,可是沒有考慮到 JVM 編譯器的指令重排安全

2、杜絕 JVM 的指令重排對單例形成的影響

一、什麼是指令重排

好比 java 中簡單的一句 instance = new Singleton,會被編譯器編譯成以下 JVM 指令:bash

memory =allocate();    //1:分配對象的內存空間 

ctorInstance(memory);  //2:初始化對象 

instance =memory;     //3:設置instance指向剛分配的內存地址
複製代碼

可是這些指令順序並不是一成不變,有可能會通過 JVM 和 CPU 的優化,指令重排成下面的順序:多線程

memory =allocate();    //1:分配對象的內存空間 

instance =memory;     //3:設置instance指向剛分配的內存地址 

ctorInstance(memory);  //2:初始化對象
複製代碼

二、影響

對應到上文的單例模式,會產生以下圖的問題:測試

  1. 當線程 A 執行完1,3,時,準備走2,即 instance 對象還未完成初始化,但已經再也不指向 null 。優化

  2. 此時若是線程 B 搶佔到CPU資源,執行  if(instance == null)的結果會是 false,spa

  3. 從而返回一個沒有初始化完成的instance對象線程

三、解決

如何去防止呢,很簡單,能夠利用關鍵字 volatile 來修飾 instance 對象,以下圖進行優化:設計

why?

很簡單,volatile 修飾符在此處的做用就是阻止變量訪問先後的指令重排,從而保證了指令的執行順序。

意思就是,指令的執行順序是嚴格按照上文的 一、二、3 來執行的,從而對象不會出現中間態。

其實,volatile 關鍵字在多線程的開發中應用很廣,暫不贅述。

雖然很贊,可是此處仍然沒有考慮過反射機制帶來的影響

3、進階篇,實現完美單例

一、小插曲

實現單例有不少種模式,在此介紹一種使用靜態內部類實現單例模式的方式:

public class Singleton {

    private static class LazyHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    private Singleton (){}

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

}
複製代碼

這是一種很巧妙的方式,起因是:

  1. 從外部沒法訪問靜態內部類 LazyHolder,只有當調用 Singleton.getInstance() 方法的時候,才能獲得單例對象 INSTANCE。

  2. INSTANCE 對象初始化的時機並非在單例類 Singleton 被加載的時候,而是在調用 getInstance 方法,使得靜態內部類 LazyHolder 被加載的時候。

  3. 所以這種實現方式是利用classloader的加載機制來實現懶加載,並保證構建單例的線程安全。

二、漏洞展現

不少種單例的寫法都有一個通病,就是沒法防止反射機制的漏洞,從而沒法保證對象的惟一性,以下舉例:

利用以下的反正代碼對上文構造的單例進行對象的建立。

public static void main(String[] args) {

    try {

        //得到構造器
        Constructor con = Singleton.class.getDeclaredConstructor();

        //設置爲可訪問
        con.setAccessible(true);

        //構造兩個不一樣的對象
        Singleton singleton1 = (Singleton)con.newInstance();
        Singleton singleton2 = (Singleton)con.newInstance();

        //驗證是不是不一樣對象
        System.out.println(singleton1);
        System.out.println(singleton2);
        System.out.println(singleton1.equals(singleton2));
    } catch (Exception e) {
        e.printStackTrace();
    }

}
複製代碼

咱們直接看結果:

結果很明顯,這顯然是兩個對象。

三、解決

使用枚舉來實現單例模式。

實現很簡單,就三行代碼:

public enum Singleton {
    INSTANCE;
}
複製代碼

上面所展現的就是一個單例,

why?

其實這就是 enum 的一塊語法糖,JVM 會阻止反射獲取枚舉類的私有構造方法

仍然使用上文的反射代碼來進行測試,發現,報錯。嘿嘿,完美解決反射的問題。

四、缺點

使用枚舉的方法是起到了單例的做用,可是也有一個弊端,

那就是  沒法進行懶加載

原文地址:www.jetchen.cn/java-single…

相關文章
相關標籤/搜索