细说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 保证了原子性却保证不了这里的有序性?(这是另一个深坑)

相关推荐
c++之路2 小时前
单例模式(Singleton Pattern)
开发语言·c++·单例模式
青山师1 天前
CompletableFuture深度解析:异步编程范式与源码实现
java·单例模式·面试·性能优化·并发编程
庞轩px3 天前
第五篇:分布式锁实战——Lua脚本原子操作与库存扣减的强一致性
redis·lua·分布式锁·synchronized·原子性·零超卖
这是程序猿3 天前
设计模式入门:Java 单例模式(Singleton)详解,从入门到实战
java·单例模式·设计模式
H Journey5 天前
C++ 多线程安全的单例模式
c++·单例模式
逝水如流年轻往返染尘6 天前
设计模式之单例模式
单例模式·设计模式
rKWP8gKv78 天前
单例模式在Java中的7种实现:从懒汉式到静态内部类
java·开发语言·单例模式
likerhood11 天前
单例模式详细讲解(java)
java·开发语言·单例模式