1. 单例模式基础概念解析
单例模式(Singleton Pattern)是设计模式中最简单却又最常被问及的模式之一。我在十多年的编程实践中发现,90%的初级开发者面试中都会涉及单例模式相关问题,但能完整说清楚其实现原理和适用场景的不到一半。
简单来说,单例模式确保一个类只有一个实例,并提供一个全局访问点。这个定义看似简单,但实际应用中需要考虑线程安全、序列化破坏、反射攻击等深层次问题。就像公司里的CEO职位,无论多少人试图创建,最终只能有一个实际的CEO存在。
注意:单例模式不同于静态工具类。静态类只有方法没有状态,而单例对象可以维护内部状态并在多次调用间共享。
2. 单例模式的五种实现方式对比
2.1 饿汉式(Eager Initialization)
java复制public class EagerSingleton {
private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton() {}
public static EagerSingleton getInstance() {
return instance;
}
}
这是最简单的实现方式,就像提前准备好的应急包,无论是否需要都会先创建好。优点是实现简单且线程安全,缺点是如果实例创建成本高且不一定会被使用,会造成资源浪费。
2.2 懒汉式(Lazy Initialization)
java复制public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static synchronized LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
这种方式像临时叫外卖,只有需要时才创建实例。虽然解决了资源浪费问题,但synchronized关键字会导致性能瓶颈,就像餐厅只有一个接单员,高峰期会出现排队现象。
2.3 双重检查锁(Double-Checked Locking)
java复制public class DCLSingleton {
private volatile static DCLSingleton instance;
private DCLSingleton() {}
public static DCLSingleton getInstance() {
if (instance == null) {
synchronized (DCLSingleton.class) {
if (instance == null) {
instance = new DCLSingleton();
}
}
}
return instance;
}
}
这是对懒汉式的优化版,像智能点餐系统——首先快速检查是否需要创建(第一次检查),真的需要时才进入同步块,进入后再次确认(第二次检查)。volatile关键字防止指令重排序导致的初始化问题。
关键点:JDK5之后volatile的语义才完善,之前版本的双重检查锁仍可能有问题
2.4 静态内部类(Initialization-on-demand Holder)
java复制public class HolderSingleton {
private HolderSingleton() {}
private static class Holder {
static final HolderSingleton INSTANCE = new HolderSingleton();
}
public static HolderSingleton getInstance() {
return Holder.INSTANCE;
}
}
这种方式利用了类加载机制保证线程安全,就像把贵重物品放在保险箱里,只有真正需要时才会打开。这是我最推荐的单例实现方式,兼具懒加载和线程安全的优点。
2.5 枚举(Enum Singleton)
java复制public enum EnumSingleton {
INSTANCE;
public void doSomething() {
// 业务方法
}
}
Joshua Bloch在《Effective Java》中推荐的方式。枚举单例天然防止反射攻击和序列化破坏,就像防弹玻璃做的展示柜,是最安全的实现方式。缺点是某些场景下不够灵活。
3. 单例模式的深层次问题
3.1 序列化破坏单例
当单例类实现Serializable接口时,反序列化会创建新实例。解决方法是在类中添加:
java复制private Object readResolve() {
return getInstance();
}
3.2 反射攻击防护
通过反射可以调用私有构造器创建新实例。防护措施是在构造器中添加检查:
java复制private Singleton() {
if (instance != null) {
throw new IllegalStateException("单例实例已存在");
}
}
3.3 多类加载器环境
不同类加载器加载的类被视为不同类,会导致单例失效。解决方法是指定类加载器或使用上下文类加载器。
4. 面试常见问题与回答策略
4.1 基础问题示例
Q:为什么要用单例模式?
A:当系统只需要一个实例来协调行为时,比如配置管理器、线程池、缓存等。确保全局唯一性同时提供集中访问点。
Q:单例模式的缺点是什么?
A:1) 难以扩展 2) 隐藏了类之间的依赖关系 3) 对单元测试不友好 4) 长期持有资源可能造成内存泄漏
4.2 进阶问题示例
Q:双重检查锁为什么要用volatile?
A:防止指令重排序导致其他线程获取到未初始化完成的对象。具体来说,new Singleton()操作分为:1) 分配内存 2) 初始化对象 3) 将引用指向内存地址。步骤2和3可能被重排序。
Q:枚举单例为何能防止反射攻击?
A:JDK底层对枚举类型的构造器有特殊处理,反射时Constructor#newInstance()方法会检查如果是枚举类型就直接抛出异常。
4.3 实战编码题
面试官常要求现场手写单例。建议按以下顺序实现并解释:
- 先写枚举方式(最安全)
- 再写静态内部类方式(最优雅)
- 最后写双重检查锁(考察细节理解)
5. 单例模式的最佳实践
5.1 适用场景判断
适合使用单例的情况:
- 需要严格控制实例数量的资源(如数据库连接池)
- 需要集中管理的配置信息
- 需要共享访问的缓存系统
不适合使用单例的情况:
- 需要多态扩展的类
- 需要频繁创建销毁的对象
- 需要mock测试的依赖项
5.2 Spring框架中的单例
Spring默认管理的bean是单例的,但与设计模式的单例有区别:
- Spring单例是容器范围内唯一,设计模式单例是JVM范围内唯一
- Spring单例可以通过配置改为原型(prototype)作用域
5.3 替代方案考虑
当发现单例模式带来测试困难或扩展问题时,可以考虑:
- 依赖注入(如通过Spring管理)
- 静态工具类(无状态场景)
- 工厂方法控制实例创建
我在实际项目中最深刻的体会是:单例模式就像编程世界里的盐,适量使用能提味,过量使用会毁掉整道菜。对于需要全局访问且确实只需要一个实例的场景,静态内部类实现是最平衡的选择;当安全性是首要考虑时,枚举实现则是终极武器。