#单例模式线程安全与序列化问题
#双重锁失效的诡异 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 + 单例 scope | IOC 容器管理 |
| 需要懒加载 + 高性能 | 静态内部类 | 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 | 能对比所有写法,知道枚举单例的优势 |