爲何我強烈推薦你用枚舉來實現單例模式

單例模式簡介

單例模式是 Java 中最簡單,也是最基礎,最經常使用的設計模式之一。在運行期間,保證某個類只建立一個實例,保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。下面就來說講Java中的N種實現單例模式的寫法。java

餓漢式

public class Singleton {

    private static Singleton instance = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return instance;
    }

}

複製代碼

這是實現一個安全的單例模式的最簡單粗暴的寫法,這種實現方式咱們稱之爲餓漢式。之因此稱之爲餓漢式,是由於肚子很餓了,想立刻吃到東西,不想等待生產時間。這種寫法,在類被加載的時候就把Singleton實例給建立出來了。apache

餓漢式的缺點就是,可能在還不須要此實例的時候就已經把實例建立出來了,沒起到lazy loading的效果。優勢就是實現簡單,並且安全可靠。設計模式

懶漢式

public class Singleton {
    
    private static Singleton instance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
    
}
複製代碼

相比餓漢式,懶漢式顯得沒那麼「餓」,在真正須要的時候再去建立實例。在getInstance方法中,先判斷實例是否爲空再決定是否去建立實例,看起來彷佛很完美,可是存在線程安全問題。在併發獲取實例的時候,可能會存在構建了多個實例的狀況。因此,須要對此代碼進行下改進。安全

public class SingletonSafe {

    private static volatile SingletonSafe singleton;

    private SingletonSafe() {
    }

    public static SingletonSafe getSingleton() {
        if (singleton == null) {
            synchronized (SingletonSafe.class) {
                if (singleton == null) {
                    singleton = new SingletonSafe();
                }
            }
        }
        return singleton;
    }
}
複製代碼

這裏採用了雙重校驗的方式,對懶漢式單例模式作了線程安全處理。經過加鎖,能夠保證同時只有一個線程走到第二個判空代碼中去,這樣保證了只建立 一個實例。這裏還用到了volatile關鍵字來修飾singleton,其最關鍵的做用是防止指令重排。markdown

靜態內部類

public class Singleton {

    private static class SingletonHolder {
        private static Singleton instance = new Singleton();
    }

    private Singleton() {
        
    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}
複製代碼

經過靜態內部類的方式實現單例模式是線程安全的,同時靜態內部類不會在Singleton類加載時就加載,而是在調用getInstance()方法時才進行加載,達到了懶加載的效果。併發

彷佛靜態內部類看起來已是最完美的方法了,其實不是,可能還存在反射攻擊或者反序列化攻擊。且看以下代碼:工具

public static void main(String[] args) throws Exception {
    Singleton singleton = Singleton.getInstance();
    Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
    constructor.setAccessible(true);
    Singleton newSingleton = constructor.newInstance();
    System.out.println(singleton == newSingleton);
}
複製代碼

運行結果:spa

經過結果看,這兩個實例不是同一個,這就違背了單例模式的原則了。線程

除了反射攻擊以外,還可能存在反序列化攻擊的狀況。以下:設計

引入依賴:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.8.1</version>
</dependency>
複製代碼

這個依賴提供了序列化和反序列化工具類。

Singleton類實現java.io.Serializable接口。

以下:

public class Singleton implements Serializable {

    private static class SingletonHolder {
        private static Singleton instance = new Singleton();
    }

    private Singleton() {

    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }

    public static void main(String[] args) {
        Singleton instance = Singleton.getInstance();
        byte[] serialize = SerializationUtils.serialize(instance);
        Singleton newInstance = SerializationUtils.deserialize(serialize);
        System.out.println(instance == newInstance);
    }

}
複製代碼

運行結果:

經過枚舉實現單例模式

在effective java(這本書真的很棒)中說道,最佳的單例實現模式就是枚舉模式。利用枚舉的特性,讓JVM來幫咱們保證線程安全和單一實例的問題。除此以外,寫法還特別簡單。

public enum Singleton {

    INSTANCE;

    public void doSomething() {
        System.out.println("doSomething");
    }

}
複製代碼

調用方法:

public class Main {

    public static void main(String[] args) {
        Singleton.INSTANCE.doSomething();
    }

}

複製代碼

直接經過Singleton.INSTANCE.doSomething()的方式調用便可。方便、簡潔又安全。

總結

以上列舉了多種單例模式的寫法,分析了其利弊之處。同時還介紹了目前最佳的單例寫法——枚舉模式,相信在將來,枚舉模式的單例寫法也會愈來愈流行。

相關文章
相關標籤/搜索