单例模式有几种写法?
结论先行:常见 5 种——饿汉、懒汉(含同步)、双重检查锁(Double-Checked Locking)、静态内部类、枚举,另有一种"容器登记"的工程用法。首选枚举(最安全),次选静态内部类(可读性好、真懒加载),DCL 必须记得加 volatile。
1. 先看结构:对外只露一个口子
┌───────────────────────────┐
│ Singleton │
│ - instance: Singleton │ ← 私有静态实例
│ - Singleton() │ ← 私有构造,外部 new 不了
│ + getInstance() │ ← 唯一访问入口
└─────────────▲─────────────┘
└── ServiceA / ServiceB / ServiceC 都从这里取
要点就三条:私有构造器、私有静态实例、公开静态获取方法。
2. 五种写法横向对比
| 写法 | 懒加载 | 线程安全 | 防反射 | 防反序列化 | 备注 |
|---|---|---|---|---|---|
| 饿汉(静态常量) | 否 | 是(类初始化保证) | 否 | 否 | 最简单,可能白造对象 |
懒汉 + synchronized | 是 | 是 | 否 | 否 | 每次调用都加锁,性能差 |
| 双重检查锁 | 是 | 是(需 volatile) | 否 | 否 | 写法复杂,易漏 volatile |
| 静态内部类 | 是 | 是 | 否 | 否 | 兼顾懒加载与简单 |
| 枚举 | 否 | 是 | 是 | 是 | JVM 保证,最佳实践 |
3. 饿汉与懒汉
// 饿汉:类加载时就建好,天然线程安全
public class ConfigHolder {
private static final ConfigHolder INSTANCE = new ConfigHolder();
private ConfigHolder() { }
public static ConfigHolder getInstance() { return INSTANCE; }
}
// 懒汉:把 INSTANCE 挪进 static synchronized 方法里延迟创建,代价是每次都抢锁
饿汉的"浪费"其实很小:只是多一个对象和一点初始化时间。真正需要懒加载的场景,是构造过程很重(读大文件、连远端)时才值得。
4. 双重检查锁(DCL)
public class DclSingleton {
// volatile 关键:禁止"分配内存 → 赋值引用"与"初始化对象"重排序
private static volatile DclSingleton instance;
private DclSingleton() { }
public static DclSingleton getInstance() {
if (instance == null) { // 第一次检查:命中时零加锁
synchronized (DclSingleton.class) {
if (instance == null) { // 第二次检查:防重复创建
instance = new DclSingleton();
}
}
}
return instance;
}
}
少写 volatile 的后果:另一线程可能拿到引用非空但字段还是默认值的半初始化对象。
5. 静态内部类:延迟加载 + 零同步
public class InnerClassSingleton {
private InnerClassSingleton() { }
// 只有第一次调用 getInstance 时才会触发 Holder 的类初始化
private static class Holder {
private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
}
public static InnerClassSingleton getInstance() {
return Holder.INSTANCE;
}
}
依赖 JVM 的类初始化机制(<clinit> 由虚拟机加锁,且只执行一次),所以既懒加载又不用自己加锁。
6. 反射与反序列化会破坏单例
// 反射绕过私有构造
Constructor<InnerClassSingleton> c = InnerClassSingleton.class.getDeclaredConstructor();
c.setAccessible(true);
InnerClassSingleton evil = c.newInstance(); // 拿到第二个实例
// 反序列化同样会 new 出一个新对象,绕过私有构造器
防护手段:构造器里判断实例已存在就抛异常;实现 readResolve() 返回已有实例。而枚举天然免疫:
public enum EnumSingleton {
INSTANCE; // JVM 保证每个枚举常量只实例化一次
public void doSomething() { /* 业务方法 */ }
}
// 调用:EnumSingleton.INSTANCE.doSomething();
《Effective Java》的结论:单元素枚举是实现单例的最佳方式——JVM 保证每个枚举常量只实例化一次,且反射与反序列化都被专门处理。
7. 容器式单例(Spring 的做法)
public class SingletonRegistry {
private static final Map<String, Object> BEANS = new ConcurrentHashMap<>();
public static Object get(String name) {
return BEANS.computeIfAbsent(name, k -> create(k)); // 不存在才创建
}
private static Object create(String name) { /* 反射创建或读配置 */ return null; }
}
Spring 的 DefaultSingletonBeanRegistry 就是"三级缓存 + 按名取单例"的容器实现。注意它管的是容器内唯一,不是 JVM 内唯一。
8. 常见追问
Q:单例在集群里还是单例吗? 不是。单例的作用域是一个 JVM/进程。集群下每个节点各有一份,若需要全局唯一,得靠分布式锁、数据库唯一约束或选主。
Q:单例有什么危害? 本质是全局状态:难 mock、隐式依赖、并发下共享可变字段容易出问题、初始化失败会拖垮类加载。所以单例对象应当尽量无状态;Spring 的单例 bean 也只保证一份实例,不保证线程安全。
小结
- 5 种写法:饿汉、懒汉、DCL、静态内部类、枚举;DCL 必须
volatile。 - 枚举最安全(防反射、防反序列化),静态内部类在"懒加载 + 简洁"上最平衡。
- 单例是 JVM 级唯一;跨节点唯一要靠分布式协调,且单例对象应尽量无状态。