一、背景
这里复习一下单例创建时的双重检查。
二、类的构造函数私有化
例如
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();
分为三步:
- 在堆上为对象分配内存空间 (给你一块地)
- 初始化对象,调用构造函数 (在地上盖房子)
- 变量指向内存空间 (贴上门牌号)
volatile 主要防止 2、3 的顺序乱序。
因为 instance 变量是被 volatile 修饰的,所以在第三步 volatile 写之前,前面两步是绝对执行过的,虽然前面可能会重排序,但是整个区域都在写之前完成就可以了, 这样就可以保证我们 instance 变量不会是 null 了。 一经发布,其他线程不会看到 null 的变量。
第一次不加锁:快速判断是否已经初始化。 第二次加锁:确保线程安全。
两次检查均发现没有初始化,则进行初始化操作。 效果:只有首次初始化需要同步,后面直接无锁返回实例。
可以在java.util.concurrent.ConcurrentHashMap中看到大量使用双重检查的案例

取出后锁定,要再次检查位置没有变更。
一个很好的比喻:检查房间是否为空然后搬进去这个过程,需要保证原子性,否则可能两个人同时搬进同一个房间!