單例模式(Singleton Pattern)是軟件開發中常用的設計模式,其核心是確保一個類在全局范圍內只有一個實例,并提供全局訪問點。在 Android 開發中,單例模式常用于管理全局資源(如網絡管理器、數據庫助手、配置中心等),避免重復創建對象造成的資源浪費。本文將詳細解析 Android 中單例模式的六種常用實現方式,對比其優缺點及適用場景,并結合 Android 特性給出最佳實踐。
一、餓漢式單例(Eager Initialization)
實現原理
在 Java 里,類的加載過程是由 JVM 嚴格把控的。當類被加載時,靜態變量會隨之初始化。餓漢式單例正是利用了這一特性,借助靜態變量來持有唯一的實例。由于靜態變量的初始化操作是在類加載階段完成的,而類加載是線程安全的,所以餓漢式單例天然具備線程安全的特性。
代碼實現
public class EagerSingleton {// 1. 私有靜態實例,類加載時創建private static final EagerSingleton INSTANCE = new EagerSingleton();// 2. 私有構造函數,禁止外部實例化private EagerSingleton() {// 初始化操作(如上下文、配置)}// 3. 公共訪問接口public static EagerSingleton getInstance() {return INSTANCE;}
}
private static final EagerSingleton INSTANCE = new EagerSingleton();
:這行代碼定義了一個私有靜態常量?INSTANCE
,在類加載時就會創建?EagerSingleton
?的實例。private EagerSingleton()
:私有構造函數防止外部代碼通過?new
?關鍵字創建新的實例。public static EagerSingleton getInstance()
:提供一個公共的靜態方法,用于獲取單例實例。
特點
- 優點:簡單直接,線程安全,無需額外同步開銷。
- 缺點:類加載時立即創建實例,即使未被使用也會占用內存(“餓漢” 命名由來)。
- 適用場景:實例占用資源少,或需要在程序啟動時初始化。
二、懶漢式單例(Lazy Initialization)
實現原理
懶漢式單例采用延遲初始化的策略,也就是在首次調用?getInstance()
?方法時才會創建實例。不過,未進行同步處理的懶漢式單例在多線程環境下是不安全的,因為多個線程可能同時判斷實例為?null
,進而創建多個實例。
非線程安全版本(危險!)
public class LazySingleton {private static LazySingleton instance;private LazySingleton() {}// 未加同步,多線程下可能返回不同實例public static LazySingleton getInstance() {if (instance == null) {instance = new LazySingleton(); // 非原子操作,可能引發競態條件}return instance;}
}
private static LazySingleton instance;
:定義一個靜態變量?instance
,初始值為?null
。if (instance == null)
:多個線程可能同時判斷?instance
?為?null
,從而進入?if
?語句塊,創建多個實例。
線程安全版本(直接同步)
public class SynchronizedLazySingleton {private static SynchronizedLazySingleton instance;private SynchronizedLazySingleton() {}// 同步整個方法,效率較低public static synchronized SynchronizedLazySingleton getInstance() {if (instance == null) {instance = new SynchronizedLazySingleton();}return instance;}
}
public static synchronized SynchronizedLazySingleton getInstance()
:使用?synchronized
?關鍵字修飾方法,保證同一時刻只有一個線程可以進入該方法,從而避免創建多個實例。
特點
- 優點:延遲初始化,節省內存(“懶漢” 命名由來)。
- 缺點:直接同步方法(
synchronized
)導致每次調用都需等待鎖,性能瓶頸明顯。 - 適用場景:單線程環境或對性能要求極低的場景(實際開發中極少使用)。
三、雙重檢查鎖定(Double-Checked Locking, DCL)
實現原理
雙重檢查鎖定模式結合了延遲初始化和線程安全的特性。通過兩次空值檢查和同步塊的使用,在減少鎖競爭的同時保證了線程安全。volatile
?關鍵字的使用是為了避免指令重排序,確保實例的初始化過程按順序執行。
代碼實現
public class DCLSingleton {// 1. volatile 禁止指令重排序,確保實例初始化完成private static volatile DCLSingleton instance;private DCLSingleton() {// 初始化操作(避免復雜邏輯,防止阻塞)}public static DCLSingleton getInstance() {// 第一次檢查:無實例時進入同步塊if (instance == null) {synchronized (DCLSingleton.class) { // 同步類對象,鎖粒度更小// 第二次檢查:避免多個線程同時通過第一次檢查if (instance == null) {instance = new DCLSingleton(); // 非原子操作,需 volatile 保證可見性}}}return instance;}
}
關鍵細節
private static volatile DCLSingleton instance;
:使用?volatile
?關鍵字修飾?instance
?變量,確保其在多線程環境下的可見性和有序性。- 第一次?
if (instance == null)
:在進入同步塊之前進行檢查,如果實例已經存在,則直接返回,避免不必要的鎖競爭。 synchronized (DCLSingleton.class)
:對類對象進行同步,確保同一時刻只有一個線程可以進入同步塊。- 第二次?
if (instance == null)
:在同步塊內部再次檢查,防止多個線程同時通過第一次檢查后創建多個實例。 volatile
?的必要性:instance = new DCLSingleton();
?這行代碼在 JVM 中實際包含三個步驟:- 分配內存空間。
- 調用構造函數初始化對象。
- 將引用賦值給?
instance
。
- 由于 JVM 可能會對指令進行重排序,導致步驟執行順序變為 1→3→2。在這種情況下,當一個線程執行完步驟 3 但還未執行步驟 2 時,另一個線程可能會判斷?
instance
?不為?null
,從而直接使用未初始化的實例,導致空指針異常。volatile
?關鍵字可以禁止指令重排序,確保步驟按順序執行。
特點
- 優點:線程安全,延遲初始化,性能高效(僅首次創建時加鎖)。
- 缺點:實現稍復雜,需正確使用?
volatile
。 - 適用場景:大多數需要延遲初始化且性能敏感的場景(如網絡管理器)。
一、核心優點
1.?確保全局唯一實例
- 避免資源重復創建:通過控制實例數量,防止多次初始化造成的資源浪費(如數據庫連接、網絡請求對象、配置管理器等)。
例:在 Android 中,若多次創建網絡管理器實例,可能導致連接池混亂或內存占用翻倍。 - 狀態全局統一:單例的唯一實例可維護全局共享狀態,確保不同模塊訪問的是同一數據(如用戶登錄狀態、應用主題配置)。
2.?提供全局訪問點
- 簡化調用邏輯:通過靜態方法(如?
getInstance()
)直接獲取實例,無需在多個模塊間傳遞對象引用,降低代碼耦合度。
例:在工具類(如日志工具、Toast 管理類)中使用單例,可在任意位置直接調用,無需頻繁傳遞實例。
3.?延遲或提前初始化控制
- 靈活的初始化策略:
- 餓漢式:類加載時立即初始化,適合資源占用小、需提前準備的場景(如全局配置類)。
- 懶漢式 / DCL:首次使用時創建實例,節省內存,適合資源占用大、非高頻使用的場景(如圖片加載引擎)。
4.?線程安全可控
- 通過合理設計(如?
synchronized
、volatile
、類加載機制),可在多線程環境下保證實例唯一性,避免競態條件。
例:DCL 模式通過雙重檢查和?volatile
?關鍵字,在高效的同時確保線程安全。
二、主要缺點
1.?內存泄漏風險(尤其在 Android 中)
- 上下文持有問題:若單例持有短生命周期對象(如?
Activity
?上下文),可能導致 Activity 無法被回收,引發內存泄漏。
// 反例:單例持有 Activity 上下文(Activity 銷毀后仍被引用)
public class BadSingleton {private Context context;private static BadSingleton instance;private BadSingleton(Context context) {this.context = context; // 若傳入 Activity 上下文,會導致泄漏}// 正確做法:使用 Application 上下文(生命周期與應用一致)
}
2.?違反單一職責原則
- 單例類可能承擔 “創建實例” 和 “業務邏輯” 的雙重職責,甚至演變為 “上帝類”,增加維護難度。
例:若網絡單例同時處理請求、緩存、日志記錄,職責過于復雜,違背 SRP(單一職責原則)。
3.?不利于單元測試
- 全局狀態難以模擬:單例的實例一旦創建,測試時難以替換為模擬對象,導致測試依賴真實環境(如數據庫、網絡)。
解決方案:通過依賴注入(如 Hilt、Dagger)或接口抽象,將單例替換為可模擬的對象。
4.?多線程復雜度與性能開銷
- 線程安全實現成本:懶漢式需額外同步機制(如?
synchronized
),可能導致性能瓶頸(如直接同步方法的低效率);DCL 模式雖優化性能,但需正確使用?volatile
?避免指令重排序。 - 初始化阻塞風險:若單例構造函數包含耗時操作(如文件讀取、網絡請求),可能阻塞主線程(尤其在 Android 的 UI 線程中)。
5.?不利于擴展與繼承
- 單例類通常通過私有構造函數禁止外部實例化,子類無法通過常規方式繼承(除非通過反射破解,但破壞封裝性)。
6.?全局狀態引發的副作用
- 單例的狀態修改可能影響所有調用方,難以追蹤問題根源(類似全局變量的弊端)。
例:若單例的配置參數被意外修改,可能導致多個模塊出現異常,且排查困難。
三、適用場景
- 資源共享且唯一的場景:
- 全局管理器(如網絡管理器、數據庫助手、文件緩存工具)。
- 配置中心、日志系統、主題管理等需要全局統一的模塊。
- 實例創建成本高的場景:
- 若對象初始化涉及復雜邏輯或耗時操作(如讀取大文件、建立網絡連接),單例可避免重復開銷。
- 簡單工具類:
- 輕量工具類(如加密工具、屏幕適配工具),通過單例提供便捷訪問入口。
感謝觀看!!!