文章目錄
- 場景通點
- 定義
- 實現思路
- 六種 Java 實現
- 餓漢式
- 懶漢式
- synchronized 方法
- 雙重檢查鎖 Double Check Lock + Volatile
- 靜態內部類 Singleton Holder
- 枚舉單例
- 單例運用場景
- 破解單例模式
- 參考
場景通點
- 資源昂貴:數據庫連接池、線程池、日志組件,只需要一份全局對象,頻繁 new 會導致資源浪費。
- 全局一致性:配置中心、緩存目錄、全局計數器等需要強唯一,避免狀態錯亂。
- 集中管理:方便做“生命周期”管控(初始化、銷毀)。
- 日志、配置中心、線程池、連接池、注冊表、全局序列號生成器等 無狀態或少狀態、需要唯一性的組件
- 資源較重而復用頻繁
定義
保證一個類只有一個實例,并提供一個全局訪問點
- “只有一個”? 私有構造器 + 類級別的持有者(靜態字段)
- “全局訪問”? 公共靜態方法 (getInstance())
實現思路
- 靜態化實例對象, 讓實例對象與 Class 對象互相綁定, 通過 Class 類對象就可以直接訪問
- 私有化構造方法, 禁止通過構造方法創建多個實例 —— 最重要的一步
- 提供一個公共的靜態方法, 用來返回這個類的唯一實例
六種 Java 實現
# | 代碼量 | 懶加載 | 線程安全 | 可靠程度 | 說明 |
---|---|---|---|---|---|
1 | 餓漢式(Eager) | ? | ? | ★★★ | 類加載即實例化,簡單直觀,無法延遲加載。 |
2 | 懶漢式(Lazy-非線程安全) | ? | ? | ★ | 只示范學習,生產別用。 |
3 | synchronized 方法 | ? | ? | ★★ | 簡單但性能差,鎖整個方法。 |
4 | DCL + volatile | ? | ? | ★★★ | 雙重檢查鎖 (Double-Checked Locking);JDK 5+ 才完全安全。 |
5 | 靜態內部類 | ? | ? | ★★★★ | JVM 類加載天生線程安全,推薦。 |
6 | 枚舉單例 | ? | ? | ★★★★★ | 反序列化 / 反射天然防護,極簡,強烈推薦。 |
餓漢式
public final class EagerSingleton {private static final EagerSingleton INSTANCE = new EagerSingleton();private EagerSingleton() {}public static EagerSingleton getInstance() { return INSTANCE; }
}
- 優點:實現最簡單、天然線程安全
Java 的語義包證了在引用這個字段之前并不會初始化它, 并且訪問這個字段的任何線程都將看到初始化這個字段所產生的所有寫入操作.
- 缺點:類加載就占用內存;若實例創建開銷大或很少用到,會浪費。
為什么餓漢式中類加載占內存?
步驟 | 發生位置 | 說明 |
---|---|---|
① 類加載(Loading) | ClassLoader | 把 .class 字節流讀進內存,創建 Class 對象,本身幾乎不耗多少內存。 |
② 鏈接 → 初始化 | JVM 執行 鏈接(驗證、準備、解析)后,進入 初始化 階段 | 所有 static 字段 在 <clinit> 方法里按源代碼順序賦值。餓漢式把單例對象定義成 private static final XXX INSTANCE = new XXX(); —— 這一行在 初始化階段立即 new 對象 |
③ 對象駐留 | Java 堆 | 無論業務代碼是否真的使用過 getInstance(),對象已被創建并常駐堆中,直到類被卸載或進程結束。 |
- “占內存”指的是 單例實例 已經分配在堆里,而不是 Class 元數據。
- 若實例本身很大(例如預加載 MB 級的配置或字典),但應用啟動后很久才用到,就屬于“浪費”。
- 餓漢式是典型的以空間換時間思想的實現: 不用判斷就直接創建, 但創建之后如果不使用這個實例, 就造成了空間的浪費. 雖然只是一個類實例, 但如果是體積比較大的類, 這樣的消耗也不容忽視.
懶漢式
線程不安全
public final class LazySingletonUnsafe {private static LazySingletonUnsafe instance;private LazySingletonUnsafe() {}public static LazySingletonUnsafe getInstance() {if (instance == null) { // ①instance = new LazySingletonUnsafe(); // ②}return instance;}
}
- 優點:節省空間, 用到的時候再創建實例對象
為什么多線程下會出問題?
競態點:假設 T1 與 T2 同時進入方法,instance
仍為 null
- T1 通過檢查(①)進入 ②,開始執行
new
,尚未完成。 - T2 同樣看到
instance == null
,也執行new
。 - 結果:生成兩個實例,違背單例約束
問題原因:沒有同步手段(鎖、volatile + CAS 等)來保證檢查與創建的 原子性
測試案例:
final class LazySingleton {private static LazySingleton instance = null;private LazySingleton() {}public static LazySingleton getInstance() {if (instance == null) {instance = new LazySingleton();}return instance;}
}public class demo {public static void main(String[] args) {Set<String> instanceSet = Collections.synchronizedSet(new HashSet<>());for (int i = 0; i < 1000; i++) {new Thread(() -> {instanceSet.add(LazySingleton.getInstance().toString());}).start();}for (String instance : instanceSet) {System.out.println(instance);}}
}
輸出結果:
如果輸出的結果中有 2 個或 2 個以上的對象, 就足以說明在并發訪問的過程中出現了線程安全問題
LazySingleton@668916a0
LazySingleton@c23df88
synchronized 方法
這樣的做法對所有線程的訪問都會進行同步操作, 有很嚴重的性能問題
public final class SynchronizedSingleton {private static SynchronizedSingleton instance;private SynchronizedSingleton() {}public static synchronized SynchronizedSingleton getInstance() {if (instance == null) {instance = new SynchronizedSingleton();}return instance;}
}
雙重檢查鎖 Double Check Lock + Volatile
public final class DCLSingleton {private static volatile DCLSingleton instance;private DCLSingleton() {}public static DCLSingleton getInstance() {// 先判斷實例是否存在if (instance == null) {// 加鎖創建實例synchronized (DCLSingleton.class) {// 再次判斷, 因為可能出現某個線程拿了鎖之后, 還沒來得及執行初始化就釋放了鎖,// 而此時其他的線程拿到了鎖又執行到此處 ==> 這些線程都會創建一個實例, 從而創建多個實例對象if (instance == null) {instance = new DCLSingleton();}}}return instance;}
}
- 為什么要雙重檢查
- volatile 關鍵詞有什么作用
使用雙重檢查的原因:
- 第一次檢查:絕大部分時間單例已存在,快速返回,避免進入
synchronized
,性能開銷 ≈0 - 第二次檢查:只有在第一次判斷為
null
、并且當前線程拿到鎖時才進入;此時仍需再判一次,防止 “T1 創建 →T2 等鎖 →T2 再創建” 的并發漏洞。(就是剛才懶漢式導致的問題)
volatile
關鍵詞:
- 可見性,保證線程中對這個變量所做的任何寫入操作對其他線程都是即時可見的,寫入
instance
對所有線程立刻可見 - 禁止 JVM 指令重排:對象創建過程實際上分三步(并不是原子性操作)
a. 分配內存,在堆內存中, 為新的實例開辟空間
b. 調用構造器初始化
c. 將引用賦給變量 (instance = address)
CPU 和編譯器可能把 b、c 重排成 c→b。可能發生以下情況:
- T1 執行到 c,引用已非 null,但對象尚未初始化;
- T2 讀取到“非 null”便返回,使用到的是
半成品對象
。
volatile
在 Java 內存模型中加了寫后讀屏障
,保證初始化完成先于賦值,徹底避免重排風險。
靜態內部類 Singleton Holder
只有在第一次調用 getInstance()
時才被加載,JVM 類加載保證線程安全 ? 懶加載 + 免鎖
public final class HolderSingleton {private HolderSingleton() {}private static class Holder {private static final HolderSingleton INSTANCE = new HolderSingleton();}public static HolderSingleton getInstance() {return Holder.INSTANCE;}
}
JVM 記載類的時候有以下步驟:① 加載 -> ② 驗證 -> ③ 準備 -> ④ 解析 -> ⑤ 初始化
JVM 在加載外部類的過程中, 是不會加載靜態內部類的, 只有內部類(SingletonHolder)的屬性/方法被調用時才會被加載, 并初始化其靜態屬性(instance)
為什么會將創建實例放在靜態內部?
核心機制:Initialization-on-demand holder idiom
- 懶加載
- 外部類 HolderSingleton 被加載時,并不會立即加載 Holder
- 只有首次調用
getInstance()
,JVM 才會解析對Holder.INSTANCE
的主動使用,從而 加載并初始化 Holder,此時才 new 實例
- 線程安全(不需要鎖)
- JVM 對 類初始化 有“互斥保證”:同一個類的
<clinit>
在多線程環境中只會執行一次,且執行期間其他線程會被阻塞。 - 實例創建天然是原子且線程安全的
- JVM 對 類初始化 有“互斥保證”:同一個類的
- 零額外開銷
- 不需要
synchronized
、不需要volatile
;除第一次觸發外,沒有任何同步損耗
- 不需要
具體原因:靜態內部類利用了 類的按需加載 + 初始化互斥,同時滿足“懶加載 + 線程安全 + 高性能”
枚舉單例
public enum EnumSingleton {INSTANCE;// 可添加字段/方法private final Map<String, String> cache = new ConcurrentHashMap<>();public void put(String k, String v) { cache.put(k, v); }public String get(String k) { return cache.get(k); }
}
- JVM 保證 序列化安全:枚舉反序列化時不會新建實例。
- 防反射:任何試圖通過 Constructor.newInstance 創建都會拋 IllegalArgumentException
- 代碼最少,可自然支持 switch
單例運用場景
框架/庫 | 場景 | 實現方式 |
---|---|---|
JDK | java.lang.Runtime | 餓漢式 |
Log4j / Logback | LoggerContext | 懶加載 + 雙檢鎖 |
Spring | 默認 Bean Scope = singleton | 容器級單例,非 GoF 模式 |
MyBatis | SqlSessionFactoryBuilder ? SqlSessionFactory | 通常一個全局實例 |
HikariCP | HikariPool 內部維護線程安全單例連接池 |
破解單例模式
- 除枚舉方式外, 其他方法都會通過反射的方式破壞單例,因此可以在構造方法中進行判斷 —— 若已有實例, 則阻止生成新的實例
private Singleton() throws Exception {if (instance != null) {throw new Exception("Singleton already initialized, 此類是單例類, 不允許生成新對象, 請通過getInstance()獲取本類對象");}
}
- 如果單例類實現了序列化接口
Serializable
, 就可以通過反序列化破壞單例,因此可以不實現序列化接口, 或者重寫反序列化方法readResolve()
// 反序列化時直接返回當前實例
public Object readResolve() {return instance;
}
Object#clone()
方法也會破壞單例, 即使你沒有實現Cloneable
接口 —— 因為 clone()方法是 Object 類中的,需要重寫方法并拋出異常
參考
- 設計模式 - Java 中單例模式的 6 種寫法及優缺點對比 - 瘦風 - 博客園