1. 引言
1.1 什么是單例模式?
單例模式(Singleton Pattern)是一種創建型設計模式,它確保一個類只有一個實例,并提供一個全局訪問點。
核心思想:控制實例化過程,避免重復創建對象。
1.2 為什么需要單例模式?
-
節省資源:某些對象(如數據庫連接池、日志管理器)只需要一個實例。
-
全局訪問:方便在多個模塊中共享數據。
-
避免沖突:如配置文件管理,防止多個實例修改導致不一致。
1.3 線程安全的重要性
-
多線程環境下,如果不加控制,可能導致:
-
創建多個實例(違反單例原則)。
-
對象狀態不一致(如緩存數據被覆蓋)。
-
-
目標:確保在任何情況下,單例類只被初始化一次。
2. 單例模式的實現方式
2.1 餓漢式(Eager Initialization)
代碼實現
public class Singleton {// 類加載時就初始化(線程安全由JVM保證)private static final Singleton INSTANCE = new Singleton();// 私有構造方法,防止外部newprivate Singleton() {}// 全局訪問點public static Singleton getInstance() {return INSTANCE;}
}
特點
-
優點:
-
實現簡單,線程安全(由類加載機制保證)。
-
沒有鎖,性能高。
-
-
缺點:
-
即使不用也會創建實例,可能浪費內存。
-
無法傳遞參數初始化(如需要動態配置)。
-
適用場景
-
實例占用內存小,且一定會被使用(如簡單配置類)。
2.2 懶漢式(Lazy Initialization)
基礎版(非線程安全)
public class Singleton {private static Singleton instance;private Singleton() {}public static Singleton getInstance() {if (instance == null) {instance = new Singleton(); // 多線程下可能創建多個實例}return instance;}
}
問題:多線程環境下,多個線程可能同時進入?if (instance == null)
,導致多次實例化。
加鎖版(線程安全但低效)
public class Singleton {private static Singleton instance;private Singleton() {}// 方法加鎖,保證線程安全public static synchronized Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;}
}
問題:每次調用?getInstance()
?都會加鎖,性能差(99%的情況不需要同步)。
2.3 雙重檢查鎖(Double-Checked Locking, DCL)
代碼實現
public class Singleton {// volatile 禁止指令重排序,避免半初始化問題private static volatile Singleton instance;private Singleton() {}public static Singleton getInstance() {if (instance == null) { // 第一次檢查(無鎖,提高性能)synchronized (Singleton.class) {if (instance == null) { // 第二次檢查(防止多線程競爭)instance = new Singleton();}}}return instance;}
}
關鍵點
-
volatile
?的作用:-
防止指令重排序(避免返回未初始化的對象)。
-
保證可見性(一個線程修改后,其他線程立即可見)。
-
-
兩次判空:
-
第一次檢查(無鎖):提高性能,避免每次加鎖。
-
第二次檢查(加鎖):防止多線程競爭導致多次實例化。
-
優點
-
線程安全?+?高性能(只有第一次初始化需要同步)。
缺點
-
代碼稍復雜,需理解?
volatile
?和指令重排序。
2.4 靜態內部類(Holder Class)
代碼實現
public class Singleton {private Singleton() {}// 靜態內部類(延遲加載)private static class SingletonHolder {private static final Singleton INSTANCE = new Singleton();}public static Singleton getInstance() {return SingletonHolder.INSTANCE; // 調用時才加載}
}
原理
-
類加載機制:
SingletonHolder
?只有在?getInstance()
?被調用時才會加載。 -
線程安全:由 JVM 保證靜態內部類的初始化是線程安全的。
優點
-
線程安全 + 延遲加載 + 無鎖高性能。
-
比雙重檢查鎖更簡潔。
缺點
-
無法傳遞參數初始化(適合無參構造)。
2.5 枚舉(Enum Singleton)
代碼實現
public enum Singleton {INSTANCE; // 單例實例public void doSomething() {System.out.println("Singleton method");}
}
原理
-
JVM 保證枚舉唯一性:枚舉實例在類加載時初始化,且不可反射創建。
優點
-
絕對線程安全(JVM 保證)。
-
防止反射攻擊(無法通過反射創建新實例)。
-
防止反序列化破壞(枚舉天然支持?
readResolve
)。
缺點
-
不能延遲加載(類加載時就初始化)。
-
寫法稍特殊(部分開發者不習慣)。
3. 如何選擇單例模式?
實現方式 | 線程安全 | 延遲加載 | 防止反射 | 防止反序列化 | 性能 | 適用場景 |
---|---|---|---|---|---|---|
餓漢式 | ? | ? | ? | ? | ??? | 簡單場景 |
懶漢式(同步) | ? | ? | ? | ? | ? | 不推薦 |
雙重檢查鎖 | ? | ? | ? | ? | ??? | 高并發 |
靜態內部類 | ? | ? | ? | ? | ??? | 推薦 |
枚舉 | ? | ? | ? | ? | ??? | 最佳實踐 |
推薦選擇:
-
簡單場景?→ 餓漢式。
-
需要延遲加載?→ 靜態內部類。
-
高并發 + 參數初始化?→ 雙重檢查鎖。
-
最佳實踐?→?枚舉單例(安全、簡潔)。
4. 單例模式的破壞與防御
4.1 反射攻擊
問題
Singleton instance1 = Singleton.getInstance();
// 反射調用私有構造方法
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton instance2 = constructor.newInstance(); // 破壞單例
防御(非枚舉類)
private Singleton() {if (INSTANCE != null) {throw new RuntimeException("Use getInstance() method!");}
}
枚舉天然防御反射(JVM 保證唯一性)。
4.2 反序列化破壞
問題
// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.ser"));
oos.writeObject(Singleton.getInstance());
// 反序列化(會調用readObject,可能創建新實例)
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.ser"));
Singleton instance2 = (Singleton) ois.readObject(); // 可能破壞單例
防御(非枚舉類)
// 添加readResolve方法,返回單例實例
protected Object readResolve() {return getInstance();
}
枚舉天然防御反序列化(JVM 保證唯一性)。
5. 總結
-
線程安全是關鍵:多線程環境下必須保證單例唯一。
-
推薦實現:
-
枚舉單例(簡潔、安全、防反射)。
-
靜態內部類(延遲加載、高性能)。
-
雙重檢查鎖(適合需要參數初始化的場景)。
-
-
避免:
-
不加鎖的懶漢式(線程不安全)。
-
方法級同步懶漢式(性能差)。
-