🔷懒加载,安全,性能,推荐程度对比
| 实现方式 | 是否懒加载 | 是否安全 | 性能 | 推荐程度 |
|---|---|---|---|---|
| 饿汉式 | ❌ | ✅ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 懒汉式 | ✅ | ❌ | ⭐⭐⭐ | ❌ 不推荐 |
| synchronized 懒汉式 | ✅ | ✅ | ⭐⭐ | ⭐ |
| DCL 双重检查 | ✅ | ✅ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 静态内部类 | ✅ | ✅ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐(最推荐) |
| 枚举 | ❌ | ✅✅✅ | ⭐⭐⭐⭐⭐ | ✅ 强安全推荐 |
🔷饿汉式
类加载的时候立即创建实例,线程安全,简单,但是美中不足的是没有办法懒加载
java
package com.zr.singleton;
/**
* @Author: zr
* @Date: 2025/11/04/17:11
* @Description:
*/
/**
* 饿汉式单例
* 特点: 类加载的时候立即创建实例,线程安全,简单,但是美中不足的是没有办法懒加载
*/
public class Singleton1 {
// JVM在类加载的时候就创建单例对象
private static final Singleton1 INSTANCE = new Singleton1();
// 获取单例对象
private static Singleton1 getInstance() {
return INSTANCE;
}
}
🔷懒汉式
第一次使用时才会创建,但是在多线程场景下会出现线程不安全的问题,但是可以懒加载
java
package com.zr.singleton;
/**
* @Author: zr
* @Date: 2025/11/04/17:16
* @Description:
*/
/**
* 懒汉式单例
* 特点: 第一次使用时才会创建
* 但是在多线程场景下会出现线程不安全的问题,
* 但是可以懒加载
*/
public class Singleton2 {
// 先不创建对象,使用的时候才创建
private static Singleton2 instance;
private Singleton2() {}
//线程不安全:多个线程可能同时进入此方法,导致创建多个实例
public static Singleton2 getInstance(){
// 第一次进入时instance为null
if (instance == null){
// 可能多个线程执行到这,导致创建多个实例
instance = new Singleton2();
}
return instance;
}
}
🔷懒汉式+synchronized
线程安全,但每次调用都加锁,性能差
java
package com.zr.singleton;
/**
* @Author: zr
* @Date: 2025/11/04/17:22
* @Description:
*/
/**
* 懒汉式单例+synchronized
* 特点: 线程安全,但每次调用都加锁,性能差
*/
public class Singleton3 {
private static Singleton3 instance;
private Singleton3() {}
// 此处加锁保证每次只有一个线程进入方法,但是这样会导致在高并发情况下性能差
public static synchronized Singleton3 getInstance(){
if (instance == null){
instance = new Singleton3();
}
return instance;
}
}
🔥DCL (Double-Check Locking)双重检查
线程安全,懒加载,性能高,实际项目中的常用方案
java
package com.zr.singleton;
/**
* @Author: zr
* @Date: 2025/11/04/17:25
* @Description:
*/
/**
* 双重检查锁DCL(Double-Check Locking)
* 特点: 线程安全,懒加载,性能高,实际项目中的常用方案
*/
public class Singleton4 {
// 必须volatile,防止指令重排序,导致线程获取未初始化的对象
private static volatile Singleton4 instance;
private Singleton4(){}
public static Singleton4 getInstance(){
// 第一重检查(无锁提升性能),第一重检查会拦截掉大部分线程,提高性能,只要instance实例化后,
// 之后所有线程都不会进入第二重检查(加锁),导致相比于懒汉式单例+synchronized性能会高很多
if (instance==null){
// 第二重检查(有锁) ,少部分线程,在第一重检查突的情况下,在此处加锁防止创建多个对象
synchronized (Singleton4.class){
if (instance==null){
instance = new Singleton4();
}
}
}
return instance;
}
}
✅ 1. 为什么 volatile 能防止指令重排?
因为 volatile 在 JVM 层加入了内存屏障(Memory Barrier),禁止了以下两类重排序:
- 写屏障(Store Barrier):禁止构造函数初始化之前,对变量的写入被重排序。
- 读屏障(Load Barrier):禁止读取到尚未初始化完成的对象引用。
也就是说:
使用 volatile 修饰对象引用,可以避免 "new 对象" 的重排序。
✅ 2. 如果没有 volatile,会发生什么指令重排?
我们看 DCL 单例中关键的这句:
java
instance = new Singleton();
这句语句在 JVM 底层不是原子操作,会被拆成 3 步:
✅ 正常的执行顺序(没有重排)
- 分配内存
- 调用构造函数进行初始化
- 将对象引用赋给 instance
✅ 可能发生的重排序顺序(危险!)
JVM 允许步骤 2 和 3 交换顺序:
- 分配内存
- ❌ 先把内存地址赋值给 instance(但对象还没初始化完成!)
- 再调用构造函数初始化
✅ 3. 多线程下导致的实际 BUG(最危险的点)
示例场景:
- 线程 A 正在执行
new Singleton(),并且发生了重排序 - 线程 B 同时进入
getInstance()并看到instance != null
✅ 此时出现问题:
线程 B 拿到的是一个 尚未初始化完成的对象 ❗
(构造函数还没执行完)
这会导致:
- 成员变量仍是默认值(null、0、false)
- 对象处于不一致/脏状态
- 随机 NPE、业务逻辑错误
- 极低概率但极难排查的并发 bug
这就是为什么双重检查锁(DCL)如果没有 volatile 会被认为是错误的实现。
✅ 4. volatile 如何解决?
加上 volatile 后:
plain
private static volatile Singleton instance;
JVM 在写入 instance 时会插入:
✅ 写屏障 :初始化完成前禁止把引用暴露出去
✅ 读屏障:禁止线程读取到一个半初始化的对象
➡️ 禁止了重排序
➡️ 也保证多线程可见性
✅ 5. 图示理解(最直观)
❌ 失败的情况(无 volatile):
plain
线程A:instance = new Singleton():
├── 分配内存
├── instance 指向这块内存(B 可见)
└── 执行构造
线程B:if (instance != null) { 使用它 } // 🚨 使用未初始化对象
✅ 正确情况(加 volatile):
plain
线程A:
分配内存
执行构造
写屏障
instance 指向这块内存(B 可见)
线程B:
读屏障保证读到的是初始化后的对象
✅ 6. 为什么静态内部类不需要 volatile?
因为 静态内部类Holder 模式依赖 JVM 类初始化,而类初始化过程:
✅ 本身是线程安全的
✅ 不会进行重排序
✅ 只执行一次
所以 静态内部类实现不需要 volatile
🔥静态内部类
- 线程安全: 由JVM保证类加载的原子性
- 懒加载: 静态内部类Holder类,只会在首次访问时才会加载
- 性能高: 无锁
java
package com.zr.singleton;
/**
* @Author: zr
* @Date: 2025/11/04/17:38
* @Description:
*/
/**
* 静态内部类单例
* 特点: 线程安全: 由JVM保证类加载的原子性
* 懒加载: Holder类,只会在首次访问时才会加载
* 性能高: 无锁
*/
public class Singleton5 {
private Singleton5(){}
//静态内部类持有单例对象,只有调用getInstance()方法时才会触发Holder类加载
private static class Holder {
private static final Singleton5 INSTANCE = new Singleton5();
}
// 获取单例对象
public static Singleton5 getInstance(){
return Holder.INSTANCE;
}
}
✅ 1. 为什么线程安全?
原因:依赖 JVM 对类加载(Class Loading) 和 初始化(Class Initialization)的严格线程安全保证。
✅ JVM 规定:类初始化是同步的(synchronized)
当某个类 第一次被主动使用 时,JVM 会对其执行初始化(initialization),具体步骤:
- 只有 一个 线程能执行该类的
<clinit>(静态代码块 + 静态变量初始化) - 其他线程必须阻塞等待
- 当初始化完成后,类被标记为 "已初始化"
- 之后任何线程访问该类,不再重复初始化
✅ 应用到 Holder 模式:
plain
public class Singleton {
private Singleton() {}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
当执行 Holder.INSTANCE 时:
➡️ JVM 会加载 Holder 类
➡️ 初始化时会执行一个 原子且线程安全的步骤 :创建 INSTANCE
➡️ 多线程环境下类只初始化一次,因此 INSTANCE 不会被创建多次
✅ 2. 为什么懒加载?
原因:静态内部类只有在第一次被使用时才会被加载。
✅ JVM 类加载触发条件(主动使用)包括:
- new
- 调用静态变量
- 调用静态方法
- 反射
- 初始化子类
但是 不包含:
- 外部类被加载
- 外部类被初始化
- 外部类实例化
✅ 对于 Holder 模式:
- Singleton 类加载时,Holder 类不会被加载
- 只有当
getInstance()被调用,真正访问Holder.INSTANCE时,
JVM 才会加载 Holder 并创建 INSTANCE
➡️ 所以达成了真正意义的 Lazy Loading(延迟加载)
✅ 最终总结
| 特性 | 原因 |
|---|---|
| 线程安全 | JVM 类初始化天然同步,保证静态变量只初始化一次 |
| 懒加载 | 静态内部类在首次使用时才会加载 |
✅ 用一句话形容静态内部类单例:
利用 JVM 类加载机制保证线程安全,用内部类延迟实例化,性能极高,不需要 synchronized。
🔥枚举
- 天生线程安全
- 防止反射攻击,防止反序列化破坏
- 写法简单
java
package com.zr.singleton;
/**
* @Author: zr
* @Date: 2025/11/04/17:43
* @Description:
*/
/**
* 枚举单例
* 特点: 天生线程安全
* 安全: 防止反射攻击,防止反序列化破坏,
* 简单: 写法简单
*/
public enum Singleton6 {
// 枚举单例
INSTANCE;
public void doSomething(){
System.out.println("Using Enum Singleton");
}
// 使用方式:
// public static void main(String[] args) {
// // 直接使用 INSTANCE
// Singleton6.INSTANCE.doSomething();
// }
}
枚举(Enum)之所以 "天生线程安全" ,不是因为它有特殊的魔法,而是因为:
JVM 对枚举的创建方式 + 类加载机制 + 枚举语义三者结合,使其必然只能生成 1 个实例,并且线程安全。
下面我按"JVM 层面的原理"给你讲透。
✅ 原因一:枚举实例在类加载阶段由 JVM 保证只初始化一次(线程安全)
枚举类在编译后,本质上会被编译成普通类:
plain
public final class Singleton extends Enum<Singleton> {
public static final Singleton INSTANCE = new Singleton();
}
关键点在这里:
INSTANCE是public static final- JVM 对 "类的初始化阶段" 有明确的线程安全保证
JVM 规范:类初始化()方法由 JVM 保证是同步的,同一时刻只能有一个线程执行。
因此:
✔ 类加载时,只会创建一次 INSTANCE
✔ 绝不会出现多个线程同时创建实例的情况
这就是天生线程安全的底层原因。
✅ 原因二:枚举构造函数只能被调用一次
普通类可以通过:
- new
- 反射
- 克隆
- 反序列化
创建多个实例。
但枚举类不行:
✅ 1. 构造器自动是 private,且只能被 JVM 调用
开发者无法 new。
✅ 2. 反射无法创建枚举对象
用反射构造会抛出异常:
plain
Cannot reflectively create enum objects
✅ 3. 枚举反序列化不会创建新实例
普通单例会被反序列化破坏,需要 readResolve() 修补。
但枚举:
反序列化时不会重新创建对象,只会返回同一个枚举常量。
✔ 你无法破坏枚举的单例性
✔ JVM 硬性保证枚举常量永远是全局唯一的对象
所以它是最牢固的单例模式。
✅ 原因三:枚举实例在 JVM 中以线程安全的方式初始化并存放在方法区
枚举常量会在类加载时初始化,并存放在方法区(或 metaspace)。
这个过程:
- 由 JVM 控制
- 原子性保证
- 且类初始化一定是线程安全
因此:
✔ 不会在多线程下初始化两次
✔ 不存在竞态条件
✔ 实例天然唯一
✅ 原因四:没有延迟加载 → 避免并发初始化问题
枚举常量不是懒加载,而是:
在类加载阶段就初始化好
这避免了懒汉式、DCL 等单例中可能遇到的并发抢初始化问题。
因此:
✔ 枚举不会有"多线程抢着初始化"的问题
✔ 懒加载单例都会面临的线程安全风险在枚举中不存在
✅总结:为什么枚举天生线程安全?
| 原因 | 说明 |
|---|---|
✅ JVM 保证类初始化 (<clinit> ) 只会执行一次 |
这是最核心原因 |
| ✅ 枚举实例在加载阶段完成初始化 | 避免并发初始化 |
| ✅ 枚举构造函数只能被 JVM 调用一次 | 保证唯一 |
| ✅ 反射不能创建枚举对象 | 防止破坏单例 |
| ✅ 反序列化不会创建新对象 | 防止序列化攻击 |
| ✅ INSTANCE 是 public static final | 和类常量一样安全且唯一 |
👉 这些保证叠加,使得枚举成为最安全、最稳固的单例实现方式,远强于 DCL、静态内部类等。