DCL双重检查锁定的陷阱与修复💣

48 阅读1分钟

看似完美的单例模式,实际上隐藏着致命的指令重排序问题!volatile是救命稻草。

一、经典DCL单例模式

错误的实现❌

public class Singleton {
    private static Singleton instance;
    
    public static Singleton getInstance() {
        if (instance == null) {              // ① 第一次检查
            synchronized (Singleton.class) { // ② 加锁
                if (instance == null) {      // ③ 第二次检查
                    instance = new Singleton(); // ④ 创建对象 💣
                }
            }
        }
        return instance;
    }
}

二、问题:指令重排序

字节码分析

instance = new Singleton();

实际执行三步:

  1. 分配内存空间
  2. 初始化对象
  3. instance指向内存

可能重排序为:

  1. 分配内存
  2. instance指向内存(此时对象未初始化!)
  3. 初始化对象

时序问题

时刻1: 线程A执行到④,重排序后先让instance!=null
时刻2: 线程B执行①,发现instance!=null
时刻3: 线程B返回instance(但对象还未初始化!)💥
时刻4: 线程B使用instance,空指针异常或数据错误

三、正确的解决方案✅

方案1:volatile(推荐)

public class Singleton {
    private static volatile Singleton instance; // volatile!
    
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

volatile作用:

  • 禁止指令重排序
  • 保证可见性

方案2:静态内部类(最优雅)

public class Singleton {
    private Singleton() {}
    
    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}

原理: 类加载机制保证线程安全。

方案3:枚举(最安全)

public enum Singleton {
    INSTANCE;
    
    public void doSomething() {
        // 业务方法
    }
}

// 使用
Singleton.INSTANCE.doSomething();

优势:

  • 天然单例
  • 防止反序列化
  • 防止反射攻击

关键总结:

  • DCL必须用volatile
  • 或用静态内部类
  • 生产推荐枚举单例

下一篇→ Exchanger的实现原理🔄