细说DCL单例模式和volatile有什么关系,volatile在DCL中是必要的吗??

文章目录

    • [1. DCL 标准代码实现](#1. DCL 标准代码实现)
    • [2. 为什么要"双重检查"?](#2. 为什么要“双重检查”?)
    • [3. 为什么必须加 `volatile`?(重头戏)](#3. 为什么必须加 volatile?(重头戏))
    • [4. Volatile 做了什么?](#4. Volatile 做了什么?)
    • [💡 八股文总结版:](#💡 八股文总结版:)

没印象太正常了,这可是 Java 并发面试里的"常青树"。DCL(Double-Checked Locking,双重检查锁定)代码看起来简单,但里面全是坑。

咱们先直接看标准的 DCL 模式代码,然后我再拆解为什么必须这么写。


1. DCL 标准代码实现

java 复制代码
public class Singleton {
    // 1. 这里的 volatile 极其重要!
    private static volatile Singleton instance;

    private Singleton() {} // 私有构造,防止外部直接 new

    public static Singleton getInstance() {
        // 2. 第一重检查:为了提高性能
        if (instance == null) {
            // 3. 加锁:确保只有一个线程进入创建逻辑
            synchronized (Singleton.class) {
                // 4. 第二重检查:为了确保线程安全
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

2. 为什么要"双重检查"?

如果不加两个 if,会有什么问题?

  • 只加外层 if(不加锁) :线程 A 和 B 同时判断为 null,都会去 new,单例直接破产。
  • 只加内层 if(加锁):每次获取实例都要先抢锁。由于抢锁是很重的操作,性能会非常差。
  • 双重检查的逻辑
  • 第一个 if 是为了拦截:如果对象已经建好了,直接返回,别去抢锁,效率最高。
  • synchronized 是为了同步:确保只有一个线程能动手去"造"这个对象。
  • 第二个 if 是为了兜底:如果线程 A 抢到锁刚造完,线程 B 刚好在锁外面排队,等 B 进去时,得再确认一眼"哥们,A 是不是已经造完了?"

3. 为什么必须加 volatile?(重头戏)

这是面试官最喜欢追问的点。即便有了 synchronized,如果不加 volatile,DCL 依然是不安全的。

核心原因:指令重排序

正如我们之前聊到的,instance = new Singleton(); 这行代码在 JVM 看来分成了三步:

  1. 分配空间(分配内存给对象)。
  2. 初始化(执行构造函数里的逻辑,比如给成员变量赋值)。
  3. 引用赋值 (把 instance 指向刚才分配的内存地址)。

编译器或 CPU 可能把顺序优化成 1 -> 3 -> 2。

灾难现场(如果没有 volatile):

  1. 线程 A 进入同步块,执行 new。由于重排序,它先执行了 1 和 3,此时 instance 已经不为 null 了,但第 2 步(初始化)还没做。
  2. 线程 B 刚好执行到第一个 if (instance == null)。它发现 instance 不为 null,于是高高兴兴地把这个半成品对象拿去用了。
  3. 结果:线程 B 调用对象方法时,可能会报空指针异常,或者因为属性没初始化而出现逻辑错误。

4. Volatile 做了什么?

instance 加上 volatile 之后,它主要起到了两个作用:

  1. 禁止重排序:它会建立一个"内存屏障"(Memory Barrier)。它强制要求:步骤 2(初始化)必须在步骤 3(赋值引用)之前完成。
  2. 建立 Happens-before 关系
  • 利用传递性(划重点!):
  • 线程 A:构造函数初始化 → h b \xrightarrow{hb} hb 写 volatile instance
  • 线程 B:读 volatile instance → h b \xrightarrow{hb} hb 后续操作
  • 结论 :根据传递性,构造函数初始化 → h b \xrightarrow{hb} hb 线程 B 的后续操作

这就保证了线程 B 拿到对象时,它一定是"熟透了"的完整对象。


💡 八股文总结版:

面试官:DCL 为什么要双重检查?为什么要加 volatile?

  • 双重检查 是为了兼顾性能和线程安全。外层 if 过滤已创建的情况,避免无意义的锁竞争;内层 if 防止多次创建。
  • 加 volatile 是为了禁止指令重排序。因为 new 对象不是原子的,如果不加 volatile,可能会出现"引用不为空但对象还没初始化完"的情况(即 1-3-2 乱序)。
  • 底层原理 是利用了 Happens-before 的程序次序规则volatile变量规则 以及传递性,确保了构造函数内的赋值操作对后续读取该实例的线程是 100% 可见的。

这一套连招下来,面试官基本上就知道你对 JMM(Java 内存模型)理解得很透彻了。

你现在对这个"半成品对象"的逻辑理解了吗?要不要我再解释一下为什么 synchronized 保证了原子性却保证不了这里的有序性?(这是另一个深坑)

相关推荐
HEADKON2 天前
司拉德帕失代偿期肝硬化及胆道梗阻患者禁止使用,肝酶升高需暂停药物
单例模式
IT空门:门主4 天前
Java 单例模式详解:7 种实现方式 + volatile 原理 + 反射与序列化问题
java·开发语言·单例模式
码农的小菜园4 天前
Java创建单例
java·开发语言·单例模式
yunn_5 天前
单例模式两种实现方法
开发语言·c++·单例模式
Shan12055 天前
浅谈:单例模式的弊端与对策
单例模式
I Promise346 天前
C++ 单例模式超详细讲解
开发语言·c++·单例模式
阿文的代码库6 天前
C++的单例模式及其作用
开发语言·c++·单例模式
Shan12056 天前
一文读懂:C++中单例模式的实现
java·c++·单例模式
Tsuki_tl7 天前
【面试高频】常见锁策略
多线程·synchronized·八股文·java面试·锁机制·悲观锁·java后端面试
老码观察7 天前
设计模式实战解读(一):单例模式——全局唯一实例的正确打开方式
单例模式·设计模式