单例模式线程安全与序列化问题

双重锁失效的诡异 bug

2020年,我们团队遇到了一个诡异的 bug:两个线程同时调用 getInstance(),居然拿到了两个不同的单例对象。

代码是这样的:

public class Singleton {
    private static Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {              // T1 检查
            synchronized (Singleton.class) {
                if (instance == null) {      // T2 检查
                    instance = new Singleton(); // 指令重排序问题
                }
            }
        }
        return instance;
    }
}

看起来有双重检查,应该安全。但实际上这段代码有严重问题。

单例模式的线程安全问题,不是加个 synchronized 就完事了。


二、线程安全问题🔴

2.1 七种单例写法对比

// 1. 饿汉式(静态常量)
class HungrySingleton {
    private static final HungrySingleton INSTANCE = new HungrySingleton();

    private HungrySingleton() {}

    public static HungrySingleton getInstance() {
        return INSTANCE;
    }
}
// ✅ 线程安全:JVM 保证
// ❌ 不是懒加载:如果构造函数耗时,拖慢启动

// 2. 饿汉式(静态代码块)
class HungryBlockSingleton {
    private static final HungryBlockSingleton INSTANCE;

    static {
        INSTANCE = new HungryBlockSingleton();
    }

    private HungryBlockSingleton() {}

    public static HungryBlockSingleton getInstance() {
        return INSTANCE;
    }
}
// ✅ 线程安全
// ❌ 不是懒加载

// 3. 懒汉式(synchronized 方法)
class LazySynchronizedSingleton {
    private static LazySynchronizedSingleton instance;

    private LazySynchronizedSingleton() {}

    public static synchronized LazySynchronizedSingleton getInstance() {
        if (instance == null) {
            instance = new LazySynchronizedSingleton();
        }
        return instance;
    }
}
// ✅ 线程安全
// ❌ 性能差:每次调用都要获取锁

// 4. 懒汉式(双重检查)
class DCLSingleton {
    private static DCLSingleton instance;

    private DCLSingleton() {}

    public static DCLSingleton getInstance() {
        if (instance == null) {
            synchronized (DCLSingleton.class) {
                if (instance == null) {
                    instance = new DCLSingleton();
                }
            }
        }
        return instance;
    }
}
// ❌ 有问题:instance = new Singleton() 不是原子操作

// 5. 双重检查 + volatile(正确写法)
class VolatileSingleton {
    private static volatile VolatileSingleton instance;

    private VolatileSingleton() {}

    public static VolatileSingleton getInstance() {
        if (instance == null) {
            synchronized (VolatileSingleton.class) {
                if (instance == null) {
                    instance = new VolatileSingleton();
                }
            }
        }
        return instance;
    }
}
// ✅ 线程安全 + 懒加载 + 高性能

2.2 为什么需要 volatile

instance = new Singleton() 在字节码层面分解为 3 步:

字节码指令序列:
0: new           #2   // 1. 分配内存
3: dup
4: invokespecial #3   // 2. 调用构造函数
7: astore_1             // 3. 写入引用

CPU 乱序执行可能导致:
  线程 T1:执行步骤 0 → 步骤 3(还没执行构造函数)
  线程 T2:看到 instance != null,直接使用(对象未初始化!)

volatile 关键字禁止指令重排序,解决这个问题。

2.3 静态内部类写法

class StaticInnerSingleton {
    // 静态内部类:延迟加载 + 线程安全
    private static class Holder {
        static final StaticInnerSingleton INSTANCE = new StaticInnerSingleton();
    }

    private StaticInnerSingleton() {}

    public static StaticInnerSingleton getInstance() {
        return Holder.INSTANCE;
    }
}
// ✅ 线程安全:JVM 类加载机制保证
// ✅ 懒加载:第一次调用 getInstance 时才加载 Holder
// ✅ 高性能:无同步开销

2.4 枚举单例(最强)

enum EnumSingleton {
    INSTANCE;

    private final String data;

    EnumSingleton() {
        data = "initialized";
    }

    public String getData() {
        return data;
    }
}
// ✅ 线程安全:JVM 保证
// ✅ 防反射:Constructor.newInstance() 对枚举抛异常
// ✅ 防序列化:ObjectInputStream 对枚举有特殊处理
// ✅ 懒加载:JVM 保证只加载一次

三、序列化问题🔴

3.1 序列化破坏单例

public class SerializableSingleton implements Serializable {
    private static final SerializableSingleton INSTANCE = new SerializableSingleton();

    private SerializableSingleton() {}

    public static SerializableSingleton getInstance() {
        return INSTANCE;
    }
}

// 测试
SerializableSingleton s1 = SerializableSingleton.getInstance();

// 序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
new ObjectOutputStream(bos).writeObject(s1);

// 反序列化
SerializableSingleton s2 = (SerializableSingleton)
    new ObjectInputStream(
        new ByteArrayInputStream(bos.toByteArray())
    ).readObject();

System.out.println(s1 == s2); // false!破坏了单例

3.2 解决方案:readResolve

public class SerializableSingleton implements Serializable {
    private static final SerializableSingleton INSTANCE = new SerializableSingleton();

    private SerializableSingleton() {}

    public static SerializableSingleton getInstance() {
        return INSTANCE;
    }

    // 反序列化时返回唯一实例
    private Object readResolve() {
        return INSTANCE;
    }
}
// ✅ 序列化不再破坏单例

四、反射攻击🔴

4.1 反射破坏单例

class ReflectAttack {
    public static void main(String[] args) throws Exception {
        // 获取构造函数
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);

        // 通过反射创建实例
        Singleton s1 = constructor.newInstance();
        Singleton s2 = constructor.newInstance();

        System.out.println(s1 == s2); // false
    }
}

4.2 防御反射攻击

class DefendedSingleton {
    private static final DefendedSingleton INSTANCE = new DefendedSingleton();

    private DefendedSingleton() {
        // 在构造函数中检查是否已经存在实例
        if (INSTANCE != null) {
            throw new RuntimeException("单例已被破坏!");
        }
    }

    public static DefendedSingleton getInstance() {
        return INSTANCE;
    }
}

4.3 枚举天然防御

// 枚举单例:天然防御反射
enum EnumSingleton {
    INSTANCE;

    EnumSingleton() {
        System.out.println("构造函数被调用");
    }
}

// 测试反射
Constructor<?> constructor = EnumSingleton.class.getDeclaredConstructors()[0];
constructor.setAccessible(true);
constructor.newInstance(); // 抛异常!
// 输出:IllegalArgumentException: Cannot reflectively create enum objects

五、生产避坑清单🟡

5.1 单例模式避坑

场景推荐写法原因
Spring Bean@Component + 单例 scopeIOC 容器管理
需要懒加载 + 高性能静态内部类JVM 保证线程安全
需要序列化枚举单例天然防反射、防序列化
简单工具类直接用 static 方法不需要单例

5.2 Spring 中的单例陷阱

// ❌ 错误:单例 Bean 中注入 prototype Bean
@Service
class SingletonService {
    @Autowired
    private PrototypeBean prototypeBean; // 注入的单例,永远是同一个

    public void doSomething() {
        prototypeBean.doIt(); // 每次调用都是同一个对象
    }
}

// ✅ 正确:使用 ObjectFactory
@Service
class CorrectSingletonService {
    @Autowired
    private ObjectFactory<PrototypeBean> prototypeBeanFactory;

    public void doSomething() {
        PrototypeBean bean = prototypeBeanFactory.getObject(); // 每次获取新对象
        bean.doIt();
    }
}

六、面试总结

级别期望回答
P5能写出 DCL + volatile 或静态内部类
P6能解释 volatile 的作用和指令重排序
P7能对比所有写法,知道枚举单例的优势