1. 引言
双重检查锁(Double-Checked Locking,简称 DCL)是 Java 单例模式中兼顾懒加载、线程安全与高性能的经典实现方案。它通过两次判空与一次加锁的组合,既避免了每次调用都加锁带来的性能损耗,又保证了多线程环境下实例的唯一性。本文将从代码实现、volatile 关键字的底层原理、优缺点分析三个维度,深入剖析 DCL 的终极优化方案。
2. 经典 DCL 实现
下面给出 DCL 单例的经典 Java 实现,代码中已标注关键设计点:
java
public class Singleton {
// volatile 禁止指令重排!核心!
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
// 第一次判断:避免每次加锁
if (instance == null) {
synchronized (Singleton.class) {
// 第二次判断:防止多线程穿透
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
3. 为什么必须加 volatile?(面试必考)
这是 DCL 实现中最容易被忽略、却又是面试官最常追问的核心问题。要理解这一点,必须先弄清楚 new 一个对象在 JVM 底层究竟经历了哪些步骤。
3.1 new 对象的底层三步
在 JVM 中,instance = new Singleton() 这行代码并非原子操作,它底层大致分为三步:
- 开辟内存空间:在堆中为 Singleton 对象分配一块内存。
- 初始化对象:执行构造函数,对对象进行初始化,为字段赋值。
- 赋值给 instance:将内存地址引用赋值给静态变量 instance。
3.2 CPU 指令重排的风险
现代 CPU 和 JIT 编译器为了提升执行效率,允许在不改变单线程语义的前提下对指令进行重排序。上述三步在重排后可能变成 1→3→2,即先分配内存并赋值引用,再执行初始化。此时若另一个线程恰好执行到第一次判空,发现 instance 不为 null,便会直接返回这个尚未完成初始化的「半初始化对象」,程序运行时就会报错。
3.3 volatile 的两大作用
- 禁止指令重排:volatile 通过内存屏障(Memory Barrier)约束了读写操作的顺序,确保赋值操作一定发生在对象初始化完成之后,从根本上杜绝了拿到半初始化对象的可能。
- 保证可见性:volatile 保证一个线程对 instance 的写入,能立即被其他线程看到,避免线程因本地缓存读到过期数据。
4. 优缺点分析
4.1 优点
- 懒加载:实例在首次调用 getInstance 时才创建,节省了类加载时的资源开销。
- 线程安全:通过 synchronized 与 volatile 双重保障,多线程环境下实例唯一且完整。
- 并发高性能:绝大多数并发调用只走第一次判空,无需进入同步块,锁竞争极小,性能接近无锁。
4.2 缺点
- 写法稍复杂:相比饿汉式或静态内部类,DCL 代码量更多,理解门槛更高。
- 需要理解 volatile:开发者必须深入理解指令重排与内存屏障,否则容易误删 volatile 而埋下隐患。
5. 总结
双重检查锁是单例模式中性能与线程安全平衡得最好的方案之一,而 volatile 正是其安全性的基石。理解 new 对象的三步底层过程与指令重排风险,是掌握 DCL 的关键。在实际开发中,若追求极致的简洁与安全,也可考虑静态内部类等替代方案,但 DCL 依然是面试与源码阅读中的高频考点。