Java 单例模式
单例模式确保一个类只有一个实例,并提供全局访问点。Java 中有四种主流实现,核心差异在于线程安全和加载时机。
选型结论
枚举实现是 Joshua Bloch 在《Effective Java》中推荐的最佳方式——简洁、线程安全、天然防反射和序列化破坏。不能用枚举时,选择静态内部类。
单例实现方式对比:
实现方式对比
| 方式 | 加载时机 | 线程安全 | 序列化防护 | 反射防护 | 推荐度 |
|---|---|---|---|---|---|
| 饿汉式 | 类加载时 | ✅ | ❌ 需手动 | ✅ 构造函数检查 | 一般 |
| 懒汉式(DCL) | 首次调用 | ✅ | ❌ 需手动 | ✅ 构造函数检查 | 推荐 |
| 静态内部类 | 首次调用 | ✅ | ✅ readResolve | ✅ 构造函数检查 | 最佳 |
| 枚举 | 类加载时 | ✅ | ✅ 天然支持 | ✅ 天然支持 | 最佳 |
饿汉式
类加载时就创建实例,JVM 保证线程安全。缺点是无论是否使用都会初始化,可能浪费资源。
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {
if (INSTANCE != null) {
throw new IllegalStateException("Already instantiated");
}
}
public static Singleton getInstance() {
return INSTANCE;
}
}懒汉式(双重检查锁 DCL)
首次调用时才创建实例,volatile 关键字防止指令重排序导致获取到未初始化的对象。
public class Singleton {
private static volatile Singleton INSTANCE = null;
private Singleton() {
if (INSTANCE != null) {
throw new IllegalStateException("Already instantiated");
}
}
public static Singleton getInstance() {
if (INSTANCE == null) { // 第一次检查(无锁)
synchronized (Singleton.class) {
if (INSTANCE == null) { // 第二次检查(有锁)
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}DCL 的关键细节
volatile 不可省略。没有它,JVM 可能对 new Singleton() 进行指令重排序,导致其他线程获取到半初始化的对象。
静态内部类
利用 JVM 类加载机制实现延迟加载和线程安全,无需 volatile 或 synchronized。
public class Singleton implements Serializable {
private Singleton() {
if (SingletonHolder.INSTANCE != null) {
throw new IllegalStateException("Already instantiated");
}
}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
// 反序列化时返回单例,防止创建新实例
private Object readResolve() {
return SingletonHolder.INSTANCE;
}
}为什么线程安全
JVM 保证类的初始化过程是线程安全的(JLS §12.4.2),SingletonHolder 只在首次被引用时加载,天然实现懒加载 + 线程安全。
枚举实现
最简洁的实现方式,天然支持序列化和反射防护。
public enum Singleton {
INSTANCE;
private String name;
public void setName(String name) { this.name = name; }
public String getName() { return this.name; }
}
// 使用
Singleton singleton = Singleton.INSTANCE;为什么枚举是最佳方式
JLS 8.9 规定枚举类型除了定义的常量外没有其他实例,序列化、克隆、反射都无法创建新实例。这是 Joshua Bloch 推荐枚举单例的核心原因。
常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 反序列化创建新实例 | Serializable 默认创建新对象 | 实现 readResolve() 返回单例;或直接用枚举 |
| 反射绕过单例 | 反射可调用私有构造函数 | 构造函数中检查实例是否已存在;或用枚举 |
| DCL 失效 | 缺少 volatile 导致指令重排序 | 声明 INSTANCE 为 volatile |
单例模式与 Java最佳实践 中的枚举比较密切相关。