单例模式是 Java 开发中最基础也最容易被面试的设计模式之一。看似简单的 "确保类只有一个实例",背后却隐藏着线程安全、反射攻击、序列化破坏等诸多陷阱。本文将用最精简的方式,带你彻底吃透单例模式的核心要点,不仅能解决开发中的实际问题,更能在面试中脱颖而出。
📌 单例模式核心概念:为什么它如此重要?
定义:确保一个类在全局范围内只有一个实例,并提供统一的访问入口。属于创建型设计模式。
核心目标:
-
资源优化:避免重复创建实例(如数据库连接池、线程池),减少内存开销。
-
全局可控:提供唯一入口,方便管理全局状态(如 Spring 容器中的 Bean)。
现实场景类比:
-
公司 CEO 办公室:全公司只有一个,所有人通过固定入口访问。
-
打印机管理:多线程环境下多个程序访问打印机,需确保唯一实例避免冲突。
🚀 5 种经典实现方式:从入门到精通
1. 饿汉式单例:简单但 "贪吃" 的实现
// 类加载时直接初始化实例,线程安全
public class HungrySingleton {
private static final HungrySingleton INSTANCE = new HungrySingleton();
private HungrySingleton() {}
public static HungrySingleton getInstance() {
return INSTANCE;
}
}
优点:简单直观,类加载阶段由 JVM 保证线程安全。
缺点:无论是否使用都会提前创建实例,可能造成内存浪费("占着茅坑不拉屎")。
2. 懒汉式单例:迟到但不安全的实现
// 第一次使用时才初始化,多线程下不安全
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
面试题陷阱:
问:为什么懒汉式在多线程下不安全?
答:当两个线程同时判断instance == null时,可能各自创建实例,导致单例失效。可通过 IDEA 的 Thread 模式调试验证。
3. 双重检查锁(DCL):高效且安全的终极方案
public class DoubleCheckSingleton {
private volatile static DoubleCheckSingleton instance;
private DoubleCheckSingleton() {}
public static DoubleCheckSingleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (DoubleCheckSingleton.class) {
if (instance == null) { // 第二次检查
instance = new DoubleCheckSingleton();
}
}
}
return instance;
}
}
核心细节:
-
volatile禁止指令重排,避免初始化未完成就被访问。 -
两次
null检查:先跳过已初始化的情况,减少锁竞争开销。
4. 静态内部类:优雅的 "懒加载" 实现
public class InnerClassSingleton {
private InnerClassSingleton() {}
// 内部类延迟加载,调用时才初始化
private static class Holder {
private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
}
public static InnerClassSingleton getInstance() {
return Holder.INSTANCE;
}
}
优势:
-
利用类加载机制保证线程安全,无需手动加锁。
-
避免饿汉式的内存浪费,实现真正的 "懒加载"。
5. 枚举式单例:《Effective Java》推荐的终极方案
public enum EnumSingleton {
INSTANCE;
// 可添加自定义方法和属性
private Object data;
public Object getData() { return data; }
public void setData(Object data) { this.data = data; }
}
面试必问题:
问:为什么枚举式单例被称为最优实现?
答:
-
天然线程安全:枚举实例由 JVM 保证唯一。
-
自动防御反射:JDK 禁止通过反射创建枚举实例。
-
序列化安全:反序列化时直接返回已有实例,无需
readResolve方法。
⚙️ 单例模式的 "致命威胁" 与解决方案
1. 反射破坏:如何阻止 "暴力创建"?
破坏代码:
// 通过反射调用私有构造方法
Class\<?> clazz = InnerClassSingleton.class;
Constructor\<?> c = clazz.getDeclaredConstructor();
c.setAccessible(true);
Object o1 = c.newInstance();
Object o2 = c.newInstance();
System.out.println(o1 == o2); // 输出false,单例被破坏
解决方案:在构造方法中添加校验:
private InnerClassSingleton() {
if (Holder.INSTANCE != null) {
throw new RuntimeException("禁止创建多个实例");
}
}
2. 序列化破坏:如何保证反序列化后实例唯一?
破坏现象:序列化后反序列化会重新创建对象。
解决方案:添加readResolve方法:
public class SerializableSingleton implements Serializable {
public static final SerializableSingleton INSTANCE = new SerializableSingleton();
private SerializableSingleton() {}
// 关键方法:反序列化时返回已有实例
private Object readResolve() {
return INSTANCE;
}
}
💡 面试高频问题与满分回答
问:单例模式在 Spring 中如何应用?
答:Spring 容器默认使用单例模式管理 Bean,通过ConcurrentHashMap存储 Bean 实例,保证全局唯一。例如ApplicationContext就是典型的单例实现。
问:ThreadLocal 是单例模式吗?它的设计思想是什么?
答:
-
不是传统单例,而是 "线程封闭" 模式:为每个线程创建独立实例。
-
原理:通过
ThreadLocalMap将实例与线程绑定,以空间换时间实现线程安全。
📚 总结:单例模式的正确打开方式
-
优先选择枚举式:代码简洁,自动解决线程安全、反射和序列化问题。
-
复杂场景用 DCL:如需要延迟加载且对性能敏感时。
-
面试核心考点:
-
各种实现的线程安全差异。
-
反射 / 序列化破坏单例的原理与解决方案。
-
框架中的实际应用(如 Spring、JDK 的
Runtime类)。
掌握这些要点,不仅能在开发中避免单例模式的坑,更能在面试中向面试官展示扎实的设计模式功底! 🚀