题目
单例双检锁的实现方式为什么要用 volatile?为什么要双检?
答案
双检锁是单例模式中兼顾线程安全和性能的经典实现,但其实现细节涉及 **Java 内存模型(JMM)** 和 **指令重排序** 等底层机制。以下从设计动机、实现原理和 `volatile` 的必要性三方面详细分析。
## 为什么要“双检”?
### 性能优化:避免无意义的锁竞争
- **第一次检查(无锁)**:当实例已存在时,直接返回,避免进入同步代码块,减少锁竞争的开销。
- **第二次检查(加锁后)**:确保即使多个线程同时通过第一次检查,只有一个线程能真正创建实例。
```java
public static DCLSingleton getInstance() {
if (instance == null) { // 第一次检查:无锁快速判断
synchronized (DCLSingleton.class) {
if (instance == null) { // 第二次检查:防止重复初始化
instance = new DCLSingleton();
}
}
}
return instance;
}
```
### 线程安全:解决竞态条件
若只有单层检查,多线程环境下可能导致:
- **多次初始化**:多个线程同时通过第一次检查,创建多个实例。
- **部分初始化对象**:未完成构造的对象被其他线程访问(由指令重排序导致)。
## 为什么必须使用 `volatile`?
### 禁止指令重排序
**对象初始化的非原子性**:`instance = new DCLSingleton()` 的实际操作分为三步:
```java
1. 分配内存空间(allocate)
2. 初始化对象(init)
3. 将引用指向内存地址(assign)
```
**问题**:编译器或 CPU 可能对步骤 2 和 3 进行重排序。若线程 A 执行顺序为 1→3→2,其他线程可能在步骤 3 后(但步骤 2 未完成)访问到未初始化的对象。
`volatile` 的作用:通过插入 **内存屏障**(Memory Barrier),禁止 JVM 对 `volatile` 变量的写操作与其之前的任何读写操作重排序,确保 `assign` 在 `init` 之后执行。
```java
private static volatile DCLSingleton instance; // 禁止指令重排序
```
### 保证可见性
`volatile` 强制线程每次访问变量时从主内存读取最新值,而非使用本地缓存(如 CPU 寄存器或缓存行),确保其他线程能立即看到实例的创建。
