单例模式(Singleton Pattern)是软件开发中常用的设计模式,其核心是确保一个类在全局范围内只有一个实例,并提供全局访问点。在 Android 开发中,单例模式常用于管理全局资源(如网络管理器、数据库助手、配置中心等),避免重复创建对象造成的资源浪费。本文将详细解析 Android 中单例模式的六种常用实现方式,对比其优缺点及适用场景,并结合 Android 特性给出最佳实践。
一、饿汉式单例(Eager Initialization)
实现原理
在 Java 里,类的加载过程是由 JVM 严格把控的。当类被加载时,静态变量会随之初始化。饿汉式单例正是利用了这一特性,借助静态变量来持有唯一的实例。由于静态变量的初始化操作是在类加载阶段完成的,而类加载是线程安全的,所以饿汉式单例天然具备线程安全的特性。
代码实现
java
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
,进而创建多个实例。
非线程安全版本(危险!)
java
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
语句块,创建多个实例。
线程安全版本(直接同步)
java
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
关键字的使用是为了避免指令重排序,确保实例的初始化过程按顺序执行。
代码实现
java
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 无法被回收,引发内存泄漏。
java
// 反例:单例持有 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. 全局状态引发的副作用
- 单例的状态修改可能影响所有调用方,难以追踪问题根源(类似全局变量的弊端)。
例:若单例的配置参数被意外修改,可能导致多个模块出现异常,且排查困难。
三、适用场景
- 资源共享且唯一的场景 :
- 全局管理器(如网络管理器、数据库助手、文件缓存工具)。
- 配置中心、日志系统、主题管理等需要全局统一的模块。
- 实例创建成本高的场景 :
- 若对象初始化涉及复杂逻辑或耗时操作(如读取大文件、建立网络连接),单例可避免重复开销。
- 简单工具类 :
- 轻量工具类(如加密工具、屏幕适配工具),通过单例提供便捷访问入口。
感谢观看!!!