單例模式簡介
單例模式是 Java 中最簡單,也是最基礎,最常用的設計模式之一。在運行期間,保證某個類只創建一個實例,保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。下面就來講講Java中的N種實現單例模式的寫法。
餓漢式
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
這是實現一個安全的單例模式的最簡單粗暴的寫法,這種實現方式我們稱之為餓漢式。之所以稱之為餓漢式,是因為肚子很餓了,想馬上吃到東西,不想等待生產時間。這種寫法,在類被加載的時候就把Singleton實例給創建出來了。
餓漢式的缺點就是,可能在還不需要此實例的時候就已經把實例創建出來了,沒起到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,其最關鍵的作用是防止指令重排。
靜態內部類
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);
}
運行結果:
通過結果看,這兩個實例不是同一個,這就違背了單例模式的原則了。
除了反射攻擊之外,還可能存在反序列化攻擊的情況。如下:
引入依賴:
<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()的方式調用即可。方便、簡潔又安全。
總結
以上列舉了多種單例模式的寫法,分析了其利弊之處。同時還介紹了目前最佳的單例寫法——枚舉模式,相信在未來,枚舉模式的單例寫法也會越來越流行。