單例模式確保類在全局僅有一個實例,並提供訪問點。懶漢式(延遲初始化)在首次調用時創建實例,但多線程環境下面臨線程安全挑戰。本文演示從不安全到雙重檢查鎖(DCL)的演進過程,並補充字節碼對象鎖原理。
public class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 線程 A、B 同時進入
instance = new Singleton(); // 創建兩個實例!
}
return instance;
}
}
問題:instance = new Singleton() 非原子操作(分配內存 → 初始化 → 賦值),指令重排可能導致其他線程看到「半初始化」對象。
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
缺點:每次調用都加鎖,並發性能極低,實際上只有首次需要同步。
public class Singleton {
private static volatile Singleton instance = null; // 關鍵:volatile
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次檢查:無鎖,高性能
synchronized (Singleton.class) { // 類級別鎖
if (instance == null) { // 第二次檢查:防止重複創建
instance = new Singleton();
}
}
}
return instance;
}
}
instance = new Singleton() 的「寫入實例引用」發生在「對象初始化完成」之後,其他線程見到的一定是完全構造好的對象。 this,類對象在類加載階段就進入 JVM,天然全局唯一,等同於 Singleton.class.getClassLoader() 級別的鎖。null 檢查避免不必要的同步開銷(實例創建後,後續調用直接返回,零鎖)。// 1. 類名.class(最常用,編譯期確定)
Class clazz1 = Singleton.class;
// 2. 實例.getClass()(運行期確定)
Class clazz2 = new Singleton().getClass();
// 3. Class.forName("全限定類名")(動態加載)
Class clazz3 = Class.forName("com.example.Singleton");
public class Singleton {
private static final Singleton INSTANCE = new Singleton(); // 類加載時創建
private Singleton() {}
public static Singleton getInstance() { return INSTANCE; }
}
| 維度 | 餓漢式 | 懶漢式 (DCL) |
|---|---|---|
| 初始化時機 | 類加載 | 首次調用 |
| 線程安全 | 天然安全 | 需 volatile + DCL |
| 資源佔用 | 可能浪費(未用也創建) | 按需創建 |
| 適用場景 | 輕量、必用對象 | 重資源、可選對象 |
public enum Singleton {
INSTANCE;
public void doSomething() { /* ... */ }
}
// 調用
Singleton.INSTANCE.doSomething();
優勢:JVM 保證單例、防反射攻擊、防序列化破壞、代碼極簡。首選方案。
volatile 語義與 Singleton.class 鎖機制,面試與代碼審查必考點。單例看似簡單,細節決定質量。