单例双重检查锁定 DCL

2025/06/17

一、背景

这里复习一下单例创建时的双重检查。

二、类的构造函数私有化

例如

public class A {
    private A() {
    }
}

注意,A 类的构造函数虽然私有化了,但是从内部还是可以进行 new 的,外部不可以。

public class A {
    public static final A INSTANCE = new A(); // 可以从内部进行 new
    private A() {
    }
}

三、双锁检测 (double-checked locking)

public class SingletonDemo {

    // volatile 防止指令重排序,并且保证变更对于其他线程可见。
    private static volatile SingletonDemo instance;

    private SingletonDemo() {}

    public static SingletonDemo getInstance() {
        // 加锁前判断一次,防止不必要的加锁,缩小锁的范围
        if (instance == null) {
            synchronized (SingletonDemo.class) {
                // 预防其他线程也创建了实例,再次判断
                if (instance == null) {
                    instance = new SingletonDemo();
                }
            }
        }
        return instance;
    }
}

这里的

instance = new SingletonDemo();

分为三步:

  1. 在堆上为对象分配内存空间 (给你一块地)
  2. 初始化对象,调用构造函数 (在地上盖房子)
  3. 变量指向内存空间 (贴上门牌号)

volatile 主要防止 2、3 的顺序乱序。

因为 instance 变量是被 volatile 修饰的,所以在第三步 volatile 写之前,前面两步是绝对执行过的,虽然前面可能会重排序,但是整个区域都在写之前完成就可以了, 这样就可以保证我们 instance 变量不会是 null 了。 一经发布,其他线程不会看到 null 的变量。

第一次不加锁:快速判断是否已经初始化。 第二次加锁:确保线程安全。

两次检查均发现没有初始化,则进行初始化操作。 效果:只有首次初始化需要同步,后面直接无锁返回实例。

可以在java.util.concurrent.ConcurrentHashMap中看到大量使用双重检查的案例

取出后锁定,要再次检查位置没有变更。

一个很好的比喻:检查房间是否为空然后搬进去这个过程,需要保证原子性,否则可能两个人同时搬进同一个房间!