双重检查锁,线程安全
使用时再进行初始化,线程安全
1. 私有静态 volatile 变量
- volatile 的作用:
- 禁止指令重排序:确保 INSTANCE = new SingletonDemo(); 这一步骤的原子性。对象的初始化分为三步(分配内存空间→初始化对象→将引用指向内存地址),若没有 volatile,JVM 可能重排序为(分配内存→引用赋值→初始化对象),此时其他线程可能访问到未完全初始化的实例。
- 确保可见性:当一个线程修改 INSTANCE 后,其他线程能立即看到最新值,避免因缓存不一致导致多次创建实例。
2. 私有构造方法
- 目的:禁止外部通过 new SingletonDemo() 直接创建实例。
- 关键点:是所有单例模式的基石,强制依赖 getINSTANCE() 方法获取对象。
3. 双重检查锁的 getINSTANCE()
第一次检查(外层 if)
- 作用:避免每次调用 getINSTANCE() 都进入同步块,降低锁竞争开销。
- 原理:如果实例已存在(INSTANCE != null),直接返回实例,无需加锁。
同步块(synchronized)
- 锁对象:使用 SingletonDemo.class 类对象作为锁。
- 目的:确保同一时刻只有一个线程能进入临界区执行实例创建。
第二次检查(内层 if)
- 防止多次实例化:多个线程可能同时通过外层检查在等待锁,第一个线程创建实例后,后续线程需再次检查INSTANCE 是否仍为 null,避免重复创建。
4. 为什么需要双重检查?
- 单次检查的问题:
- 如果仅在外层检查:无法应对多线程同时首次调用的情况,可能导致多个线程都通过 if (INSTANCE == null) 检查,最终重复创建实例。
- 如果仅在内层检查:每次调用都需要加锁,即使实例已存在,增加性能开销。
5. 指令重排序与 volatile 的必要性
假设无 volatile 修饰:
这行代码可能被 JVM 优化为:
- 分配内存空间。
- 将 INSTANCE 引用指向内存地址(此时 INSTANCE != null)。
- 初始化对象(构造函数执行)。
如果线程 A 执行到步骤 2(INSTANCE 已为非 null),但未执行步骤 3,此时线程 B 调用 getINSTANCE():
外层检查 INSTANCE != null,直接返回未初始化完成的实例 → 程序错误、空指针异常等。
volatile 通过禁止指令重排序(通过内存屏障)确保 new SingletonDemo() 原子性:
- 分配内存空间。
- 初始化对象。
- 将 INSTANCE 指向对象地址。