单例模式分为懒汉式和饿汉式
饿汉式单例
类加载时就立即创建单例对象,
public class HungrySingleton {
// 1. 类加载时就创建对象
private static final HungrySingleton instance = new HungrySingleton();
// 2. 构造器私有化,防止外部创建
private HungrySingleton() {
}
// 3. 对外提供获取实例的静态方法
public static HungrySingleton getInstance() {
return instance;
}
}
运行机制
当 JVM 加载 HungrySingleton 类时:
- 静态变量
instance被初始化; - 立即执行
new HungrySingleton(); - 所以类加载完毕后,
instance已经是一个现成的单例对象。
无论调用多少次 getInstance() ,都返回同一个对象。
举例
public class DatabaseConnectionPool {
private static final DatabaseConnectionPool pool = new DatabaseConnectionPool();
private DatabaseConnectionPool() {
System.out.println("连接池创建完成");
}
public static DatabaseConnectionPool getInstance() {
return pool;
}
}
当程序启动时,连接池就被提前创建,后续各个模块直接用,不需要再判断或加锁。
非常适合像“日志器”、“配置加载器”、“线程池”等对象
懒汉式单例
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) { // 第一次调用才创建
instance = new LazySingleton();
}
return instance;
}
}
饿汉-懒汉 对比
| 特性 | 懒汉式 | 饿汉式 |
|---|---|---|
| 创建时机 | 第一次使用时 | 类加载时 |
| 是否线程安全 | ❌(需加锁) | ✅ JVM保证 |
| 性能 | ⚠️ 稍低(加锁/DCL) | ✅ 高(无锁) |
| 延迟加载 | ✅ 是 | ❌ 否 |
| 代码复杂度 | ⚠️ 稍复杂 | ✅ 简单 |
| 推荐写法 | ✅ DCL 或静态内部类 | ✅ 普通静态初始化 |
为什么懒汉式下线程不安全?
如果是单线程模式下,懒汉式没有线程安全问题。
但是如果是多线程模式下,就会出现问题。
初始化对象不是原子操作
instance = new LazySingleton();它有三个步骤;
- 分配内存空间
- 调用构造方法,初始化对象
- 将引用指向内存地址(instance指向这个对象)
编译器/CPU 可能发生 指令重排,顺序变为:
1️⃣ 分配内存
3️⃣ 指向地址
2️⃣ 初始化对象
在多线程模式下, 会导致另一个线程出错 :
假设线程 A 正在执行上面 3 步:
- A 执行到第 3 步(引用已经指向地址,但对象还没初始化完)
- 线程 B 此时执行
if (instance == null)→ 判断为 false,直接返回这个“半初始化对象”
→ 这会引发 空指针 或 对象状态异常
如何解决?
1. 加 synchronized 锁
整个方法加锁,性能开销大 ; 每次获取实例都要加锁,即使实例已经创建完毕。
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
// 整个方法加锁,线程安全
public synchronized static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
2.加上volatile
volatile 会禁止指令重排
确保初始化的顺序 严格按 1 → 2 → 3 执行。
同时也保证了 instance 的 可见性(线程 B 能立刻看到线程 A 初始化完的结果)。
「懒汉式单例 + 双重检查锁定 DCL」
public class Singleton {
private static volatile Singleton instance; // volatile 关键
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 非原子操作
}
}
}
return instance;
}
}
synchronized (Singleton.class) 对这个类加锁;
为什么需要二次检查instance == null?第一次是判断是否已经被创建了,第二次是防止在第一次检查完是否创建,在加锁的前瞬间给其他线程船创建了。
与单纯的加锁synchronized的锁粒度不同,只有在第一次创建对象的时候加锁。
3.静态内部类单例
静态内部类单例不需要使用 volatile ,也天然不会被指令重排破坏。
原因:
静态内部类的实例化过程是由 JVM 类加载机制 保证线程安全的,
JVM 在类加载和初始化阶段就会自动加锁并禁止指令重排。
public class Singleton {
private Singleton() {}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
二者比较
| 特性 | 双重检查锁(DCL) | 静态内部类 |
|---|---|---|
| 是否懒加载 | ✅ 是 | ✅ 是 |
| 是否需 volatile | ✅ 必须 | ❌ 不需要 |
| 线程安全性 | 依赖 volatile 保证 | JVM 类加载机制天然保证 |
| 是否可能被指令重排破坏 | 是(未加 volatile) | 否 |
| 性能 | 较好(但有锁判断) | 最优(无锁) |
反射可以破环单例
哪怕加了 volatile,也只能防止“线程安全问题”,但它防不了“暴力反射”。
例如:
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton s1 = constructor.newInstance();
Singleton s2 = constructor.newInstance();
System.out.println(s1 == s2); // ❌ false
因为反射可以直接调用私有构造函数,绕过你的单例逻辑。
静态内部类单例也可能被破坏
反射仍可通过 newInstance() 调用私有构造函数创建多个实例。
解决办法之一:
public class Singleton {
private static volatile Singleton instance;
// 用于标记是否已被实例化
private static boolean initialized = false;
private Singleton() {
synchronized (Singleton.class) {
if (initialized) {
throw new RuntimeException("不要试图通过反射破坏单例!");
}
initialized = true;
}
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
原理分析:
- 第一次调用
getInstance()时,构造函数执行initialized = true; - 若再用反射调用构造函数:
-
- 会发现
initialized == true; - 构造函数抛异常,拒绝创建第二个实例。
- 会发现
但是还是可以被破坏
反射修改 initialized 标志位; 那么依旧可以修改对应的信息导致破坏。
使用 Unsafe.allocateInstance()(绕过构造函数直接分配内存
枚举单例 —— 唯一不会被破坏的单例模式
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("Doing something...");
}
}
什么是枚举(Enum)
枚举是一种特殊的类:
- 每个枚举常量都是枚举类的 唯一实例
- 枚举的构造器默认是 private
- JVM 确保它只会被实例化一次
- 自动实现
Serializable和Comparable接口
为什么枚举单例不会被破坏
1. 防反射
JDK 源码中对 Enum 的反射构造有特殊保护:
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
所以反射创建枚举实例会抛异常。
2.防反序列化
- 普通单例可被反序列化破坏(通过 readObject 创建新对象);
- 但枚举类型在反序列化时,JVM 内部使用
Enum.valueOf(),保证同一个实例。
3.线程安全 & 懒加载
- 枚举类在第一次使用时才被加载(懒加载);
- JVM 保证类加载过程的线程安全性。
总结
| 单例模式类型 | 懒加载 | 线程安全 | 防反射 | 防反序列化 | 是否推荐 |
|---|---|---|---|---|---|
| 饿汉式 | 否 | ✅ | ❌ | ❌ | 一般 |
| 懒汉式 (同步方法) | ✅ | ✅ | ❌ | ❌ | 一般 |
| DCL + volatile | ✅ | ✅ | ❌ | ❌ | 常用 |
| 静态内部类 | ✅ | ✅ | ❌ | ❌ | 推荐 |
| 枚举单例 | ✅ | ✅ | ✅ | ✅ | ✅✅✅ 推荐首选 |
这个串联其:volatile → 单例模式 → 反射破坏 → 枚举防御 JMM与Volatitle它并非真实存在的物理内存划分,而是Java虚拟机(JVM)定义的一套规则,用来屏蔽各种硬用来屏 - 掘金