六种核心实现方式对比
1. 饿汉式单例(Eager Initialization)
原理:利用类加载时静态变量初始化的特性,天然线程安全。
代码:
public class EagerSingleton {
private static final EagerSingleton INSTANCE = new EagerSingleton();
private EagerSingleton() {} // 私有构造防止外部实例化
public static EagerSingleton getInstance() { return INSTANCE; }
}
特点:
- 优点:简单可靠,线程安全,无需额外同步。
- 缺点:类加载即初始化,浪费内存(适合小资源实例)。
- 适用场景:程序启动时需初始化的全局配置类。
2. 懒汉式单例(非线程安全 vs 线程安全)
非线程安全(危险):
public class LazySingleton {
private static LazySingleton instance;
public static LazySingleton getInstance() {
if (instance == null) instance = new LazySingleton(); // 多线程下可能创建多个实例
return instance;
}
}
线程安全(同步方法):
public class SynchronizedLazySingleton {
private static SynchronizedLazySingleton instance;
public static synchronized SynchronizedLazySingleton getInstance() {
if (instance == null) instance = new SynchronizedLazySingleton();
return instance;
}
}
特点:
- 优点:延迟初始化,节省内存。
- 缺点:同步方法性能差(每次调用都加锁),实际开发极少使用。
3. 双重检查锁定(DCL,线程安全)
核心代码:
public class DCLSingleton {
private static volatile DCLSingleton instance; // volatile 禁止指令重排序
public static DCLSingleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (DCLSingleton.class) { // 同步类对象,缩小锁范围
if (instance == null) { // 第二次检查
instance = new DCLSingleton(); // 非原子操作,需 volatile 保障
}
}
}
return instance;
}
}
关键细节:
volatile
防止指令重排序(如先赋值再初始化导致的空指针风险)。- 适用场景:性能敏感且需延迟初始化的场景(如网络管理器)。
4. 静态内部类单例(推荐)
原理:利用静态内部类的类加载机制(JVM 保证线程安全),延迟初始化。
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton() {}
private static class Holder {
static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance() {
return Holder.INSTANCE; // 首次调用时加载 Holder 类,触发实例化
}
}
特点:
- 兼顾饿汉式的线程安全与懒汉式的延迟初始化,实现优雅。
- Android 中常用于工具类或轻量管理器。
5. 枚举单例(最安全)
Java 官方推荐(《Effective Java》):
public enum EnumSingleton {
INSTANCE; // 枚举常量天然唯一,线程安全
// 业务方法
public void doSomething() { /* ... */ }
}
优势:
- 自动支持序列化和反序列化,防止反射攻击(
Enum
禁止通过反射创建实例)。 - 代码极简,无需额外处理线程安全。
6. Kotlin 单例(简洁高效)
伴生对象 + lazy 代理(懒汉式):
class KotlinSingleton {
companion object {
val instance: KotlinSingleton by lazy { KotlinSingleton() } // 线程安全,延迟初始化
}
}
枚举实现:
enum class KotlinEnumSingleton {
INSTANCE;
fun doSomething() = Unit
}
优势:
by lazy
简化 DCL 逻辑,默认线程安全(可配置LazyThreadSafetyMode
)。- 枚举语法更简洁,天然防御反射和反序列化。
面试追问
一、基础概念与实现对比类真题
真题 1:饿汉式和懒汉式单例的核心区别是什么?适用场景如何选择?
解答:
核心区别:
- 初始化时机:饿汉式在类加载时立即创建实例(静态变量初始化),懒汉式在首次调用
getInstance()
时创建(延迟初始化)。 - 线程安全:饿汉式依赖 JVM 类加载机制,天然线程安全;非同步懒汉式多线程下不安全,需通过
synchronized
或 DCL 保证安全。 - 内存占用:饿汉式无论是否使用都占用内存(“饿”),懒汉式节省内存但实现复杂(“懒”)。
- 初始化时机:饿汉式在类加载时立即创建实例(静态变量初始化),懒汉式在首次调用
适用场景选择:
- 若实例占用资源少且希望提前初始化(如全局配置类),选饿汉式(简单可靠)。
- 若实例创建耗时 / 耗资源(如网络管理器、图片加载引擎),且需延迟初始化,选DCL 懒汉式(兼顾性能与线程安全)。
- 直接同步方法的懒汉式(
synchronized
修饰方法)因性能差,实际开发几乎不用,仅作为概念对比。
考点分析:
考察对两种模式核心原理的理解,需结合 “类加载机制”“线程安全实现”“性能与内存权衡” 展开,避免仅停留在代码表面。
真题 2:DCL 单例为什么需要 volatile
?不加会有什么问题?
解答:
指令重排序风险:
instance = new DCLSingleton();
实际分三步:- 分配内存空间(
memory = allocate()
); - 调用构造函数初始化对象(
ctorInstance(memory)
); - 将引用赋值给
instance
(instance = memory
)。
JVM 可能优化为 1→3→2(重排序),若线程 A 执行到步骤 3 时(instance
非空但未初始化),线程 B 调用getInstance()
发现instance
非空,直接返回未初始化的实例,导致空指针异常或逻辑错误。
- 分配内存空间(
volatile
的作用:
禁止指令重排序,确保步骤按 1→2→3 执行;同时保证多线程间instance
的可见性(一个线程修改后,其他线程立即感知)。
反例代码:
若不加 volatile
,在高并发下可能返回未初始化的实例,典型面试陷阱!
考点分析:
深入考察 JVM 底层机制(指令重排序、可见性)与多线程安全,需结合底层原理解释,避免仅回答 “防止指令重排序” 的表面原因。
二、Android 特性与实战问题类真题
真题 3:Android 中使用单例时,如何避免内存泄漏?举例说明错误与正确做法。
解答:
错误案例(Activity 上下文泄漏):
public class BadSingleton { private Context context; // 持有 Activity 上下文(短生命周期) private static BadSingleton instance; private BadSingleton(Context context) { this.context = context; // 若传入 Activity,Activity 销毁后仍被单例引用,无法回收 } public static BadSingleton getInstance(Context context) { if (instance == null) { instance = new BadSingleton(context); } return instance; } }
问题: Activity 销毁时,单例仍持有其引用,导致 Activity 无法被 GC 回收,内存泄漏。
正确做法:
- 使用 Application 上下文(生命周期与应用一致):
public class GoodSingleton { private Context context; private static GoodSingleton instance; private GoodSingleton(Context context) { this.context = context.getApplicationContext(); // 或直接传入 Application } public static GoodSingleton getInstance(Context context) { if (instance == null) { instance = new GoodSingleton(context); } return instance; } }
- 若必须使用 Activity 上下文,确保单例不长期持有(如临时方法参数,而非成员变量)。
- 使用 Application 上下文(生命周期与应用一致):
考点分析:
结合 Android 组件生命周期(Activity 短生命周期 vs Application 长生命周期),考察内存泄漏的根本原因及预防措施,是 Android 面试必考点。
真题 4:单例模式在 Android 中常用于哪些场景?举 3 个实际例子。
解答:
- 全局管理器:
- 网络请求管理器(如 Retrofit 封装类,统一管理 OkHttp 连接池);
- 数据库助手(如 Room Database 的
DatabaseClient
,避免重复创建连接)。
- 配置与状态管理:
- 应用主题 / 语言配置中心(存储全局配置,跨页面同步);
- 用户登录状态管理器(确保各模块获取同一登录状态)。
- 轻量工具类:
- 日志工具(如统一的 Logger 类,控制日志级别和输出渠道);
- Toast 管理类(避免多次创建 Toast 实例,保证显示顺序)。
扩展: 需说明选择单例的原因 —— 避免资源重复创建、提供全局访问点、维护统一状态,而非简单罗列场景。
三、线程安全与破坏防御类真题
真题 5:单例模式如何防止反射攻击?
解答:
反射可通过 newInstance()
调用私有构造函数创建新实例,破坏单例唯一性。防御措施:
- 在构造函数中检查实例是否已存在:
private static volatile Singleton instance; private Singleton() { if (instance != null) { // 防止反射多次创建 throw new IllegalStateException("Instance already exists"); } }
注意:首次通过反射创建时,instance
为null
,可正常创建;再次反射时触发检查。 - 枚举单例(终极防御):
Java 枚举天然防止反射创建实例(Enum.newInstance()
会抛出异常),是最安全的单例实现:public enum EnumSingleton { INSTANCE; // 业务方法 }
考点分析:
考察单例的鲁棒性,需区分普通单例(需手动防御)与枚举单例(天然免疫)的差异,枚举是 Java 官方推荐的安全单例方式
真题 6:反序列化会破坏单例吗?如何解决?
解答:
- 问题: 反序列化时,
ObjectInputStream.readObject()
会创建新对象,导致单例失效。 - 解决方案:
- 添加
readResolve()
方法,返回现有实例:private Object readResolve() { return instance; // 返回单例实例,避免创建新对象 }
- 枚举单例无需额外处理,反序列化时直接返回枚举常量,天然支持单例。
- 添加
代码示例:
public class SerializableSingleton implements Serializable {
private static final long serialVersionUID = 1L;
private static final SerializableSingleton INSTANCE = new SerializableSingleton();
private SerializableSingleton() {}
public static SerializableSingleton getInstance() { return INSTANCE; }
// 防止反序列化创建新实例
protected Object readResolve() { return INSTANCE; }
}
考点分析:
考察对 Java 序列化机制的理解,需明确单例在反序列化场景下的漏洞及修复方法,结合 readResolve
的作用深入解释。