外观
一句话答案
保证类只有一个实例,推荐枚举实现(最安全)或双重检查锁 DCL(volatile 禁止重排防止获取未初始化对象)。
核心要点
单例模式确保一个类只有一个实例,并提供全局访问点。常见写法有四种:
1. 饿汉式(推荐用于简单场景)
java
public class Singleton {
// 类加载时即初始化,JVM 保证线程安全
private static final Singleton INSTANCE = new Singleton();
private Singleton() {} // 私有构造
public static Singleton getInstance() {
return INSTANCE;
}
}- 优点:实现简单,线程安全(JVM 保证 static 变量只初始化一次)
- 缺点:类加载就实例化,不支持懒加载
2. 懒汉式 DCL(Double-Checked Locking)
java
public class Singleton {
// 必须加 volatile!
private static volatile Singleton INSTANCE;
private Singleton() {}
public static Singleton getInstance() {
if (INSTANCE == null) { // 第一次检查:避免不必要的加锁
synchronized (Singleton.class) {
if (INSTANCE == null) { // 第二次检查:防止重复创建
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}DCL 为什么必须加 volatile?
INSTANCE = new Singleton() 不是原子操作,实际分三步:
- 分配内存空间
- 初始化对象(调用构造方法)
- 将引用指向分配的内存
JVM 可能对步骤 2 和 3 进行指令重排序,变成 1→3→2。如果线程 A 执行了 1→3 但还未执行 2,此时线程 B 在第一次检查时发现 INSTANCE != null,直接返回了一个未完成初始化的对象,导致 NPE 或数据异常。
volatile 通过内存屏障禁止指令重排序,保证 1→2→3 的顺序执行。→ 详见 Module 04 JMM 部分
3. 静态内部类(推荐)
java
public class Singleton {
private Singleton() {}
// 静态内部类在外部类加载时不会被加载,实现懒加载
// 内部类加载时 JVM 保证线程安全
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}- 兼具懒加载 + 线程安全,无锁性能好
4. 枚举(终极写法,Effective Java 推荐)
java
public enum Singleton {
INSTANCE;
public void doSomething() {
// 业务方法
}
}- JVM 保证唯一实例
- 天然防止反射攻击和反序列化破坏单例
- 缺点:不支持懒加载,写法不够直观
对比总结:
| 写法 | 线程安全 | 懒加载 | 防反射 | 推荐度 |
|---|---|---|---|---|
| 饿汉式 | 是 | 否 | 否 | ★★★ |
| DCL | 是(需 volatile) | 是 | 否 | ★★★★ |
| 静态内部类 | 是 | 是 | 否 | ★★★★★ |
| 枚举 | 是 | 否 | 是 | ★★★★★ |
追问与易错
追问方向:
- DCL 为什么需要 volatile?(防止对象未初始化就被使用,指令重排问题)
- 枚举单例为什么最安全?(天然防反射和序列化攻击)
- Spring 的单例和设计模式的单例有什么区别?(Spring 是容器级别的单例,不是类级别)
易错点:
- ❌ "饿汉式有性能问题"——类不使用不会加载,实际很少浪费
- ❌ "DCL 不加 volatile 也行"——JDK5 之前确实有问题(指令重排)
💡 记忆锚点
全校只许有一个校长:饿汉式开学就任命(简单但不懒加载),DCL是有人找校长时才选(volatile防选到还没穿好校服的),静态内部类是密封信封里的任命书(拆封才生效),枚举是写入宪法的终身制(防篡改防克隆最安全)。