题目
设计线程安全的延迟初始化方案并分析JMM原理
信息
- 类型:问答
- 难度:⭐⭐⭐
考点
Java内存模型(JMM),happens-before原则,volatile语义,双重检查锁定(DCL),内存屏障
快速回答
实现线程安全的延迟初始化需考虑:
- 使用
volatile修饰实例引用防止指令重排序 - 结合
synchronized保证原子性 - 利用JMM的happens-before规则确保可见性
典型方案:双重检查锁定(DCL)模式,需注意:
- JDK 5+的
volatile修复了DCL问题 - 静态内部类方案是更安全的替代方案
问题场景
在并发环境下实现高性能的单例延迟初始化,要求:
1. 线程安全
2. 仅当首次访问时初始化
3. 避免同步性能开销
核心原理
Java内存模型(JMM)关键点:
- 指令重排序:编译器/处理器可能优化指令顺序
- 可见性问题:线程本地缓存导致状态更新不可见
- happens-before:volatile写先于后续读,锁释放先于后续获取
- 内存屏障:volatile插入LoadStore屏障防止重排序
代码示例与缺陷分析
// 错误实现:非线程安全
class UnsafeSingleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 检查1(无同步)
synchronized (Singleton.class) {
if (instance == null) { // 检查2
instance = new Singleton(); // 问题点:可能重排序
}
}
}
return instance;
}
}问题根源:
对象初始化new Singleton()包含三个步骤:
1. 分配内存空间
2. 初始化对象
3. 将引用赋值给变量
步骤2和3可能被重排序,导致其他线程获取到未初始化的对象。
正确解决方案
方案1:volatile修复DCL(JDK5+)
class SafeSingleton {
private static volatile Singleton instance; // 关键:volatile修饰
public static Singleton getInstance() {
if (instance == null) {
synchronized (SafeSingleton.class) {
if (instance == null) {
instance = new Singleton(); // volatile禁止重排序
}
}
}
return instance;
}
}volatile作用机制:
1. 写操作前插入StoreStore屏障
2. 写操作后插入StoreLoad屏障
3. 读操作前插入LoadLoad屏障
4. 读操作后插入LoadStore屏障
确保初始化完成前引用不可见。
方案2:静态内部类(推荐)
class HolderSingleton {
private HolderSingleton() {}
private static class Holder {
static final HolderSingleton INSTANCE = new HolderSingleton();
}
public static HolderSingleton getInstance() {
return Holder.INSTANCE; // 类加载机制保证线程安全
}
}JVM类加载特性:
1. 类初始化阶段执行静态初始化
2. JVM保证类初始化过程的互斥性
3. 首次访问Holder类时触发初始化
最佳实践
- 优先使用静态内部类方案(简单安全)
- 若需DCL必须用
volatile修饰实例变量 - 避免在构造函数中泄漏
this引用 - 考虑枚举单例(
Enum Singleton)方案
常见错误
- 忘记
volatile修饰符(概率性BUG) - 在构造函数中启动线程(导致
this逸出) - 误用
final字段(不解决重排序问题) - 过度同步导致性能下降
扩展知识
- JMM与CPU内存模型:MESI协议 vs JMM抽象
- final字段特殊规则:正确构造的对象中final字段可见性保证
- VarHandle:JDK9+提供的细粒度内存控制
- 替代方案:使用
java.util.concurrent中的LazyInitializer