单例模式的核心是保证类的实例全局唯一,但在一些特殊场景下,这个约束会被突破,下面结合代码示例说明原因和解决办法。

1. 反射机制破坏单例

原因:单例模式通常会把构造方法私有化(private),但Java的反射机制可以强制访问私有构造方法,从而创建新的实例,直接破坏单例。 代码示例(破坏过程)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 经典的懒汉式单例(未做防反射处理)
class LazySingleton {
private static LazySingleton instance;

// 私有构造方法
private LazySingleton() {}

public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}

// 测试反射破坏单例
public class TestReflect {
public static void main(String[] args) throws Exception {
// 获取正常的单例实例
LazySingleton instance1 = LazySingleton.getInstance();

// 通过反射获取私有构造器,创建新实例
Class<LazySingleton> clazz = LazySingleton.class;
java.lang.reflect.Constructor<LazySingleton> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true); // 强制访问私有构造器
LazySingleton instance2 = constructor.newInstance();

// 结果:false,说明创建了两个不同的实例
System.out.println(instance1 == instance2);
}
}

解决办法:在私有构造方法中添加校验,若已有实例则抛出异常,阻止反射创建新实例:

1
2
3
4
5
6
private LazySingleton() {
// 防反射:如果已有实例,直接抛异常
if (instance != null) {
throw new RuntimeException("禁止通过反射创建多个实例");
}
}

2. 序列化/反序列化破坏单例

原因:如果单例类实现了Serializable接口,当把单例对象序列化后写入文件,再反序列化读取时,会创建一个全新的实例(反序列化会绕过构造方法)。 代码示例(破坏过程)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.io.*;

// 实现Serializable的单例类
class SerializableSingleton implements Serializable {
// 饿汉式单例
private static final SerializableSingleton instance = new SerializableSingleton();

private SerializableSingleton() {}

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

// 测试序列化破坏单例
public class TestSerialize {
public static void main(String[] args) throws Exception {
SerializableSingleton instance1 = SerializableSingleton.getInstance();

// 序列化到文件
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.txt"));
oos.writeObject(instance1);
oos.close();

// 反序列化读取
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.txt"));
SerializableSingleton instance2 = (SerializableSingleton) ois.readObject();
ois.close();

// 结果:false,反序列化创建了新实例
System.out.println(instance1 == instance2);
}
}
解决办法:在单例类中添加readResolve()方法,该方法会在反序列化时被调用,返回已有的单例实例:
1
2
3
4
// 反序列化时调用此方法,返回已有实例
private Object readResolve() {
return instance;
}

3. 克隆(clone)方法破坏单例

原因:如果单例类实现了Cloneable接口,调用clone()方法会创建新的实例(clone()默认是浅拷贝,不会调用构造方法)。 代码示例(破坏过程)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 实现Cloneable的单例类
class CloneableSingleton implements Cloneable {
private static CloneableSingleton instance = new CloneableSingleton();

private CloneableSingleton() {}

public static CloneableSingleton getInstance() {
return instance;
}

// 重写clone方法(默认继承自Object,protected修饰)
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // 调用父类的clone,创建新实例
}
}

// 测试克隆破坏单例
public class TestClone {
public static void main(String[] args) throws Exception {
CloneableSingleton instance1 = CloneableSingleton.getInstance();
CloneableSingleton instance2 = (CloneableSingleton) instance1.clone();

// 结果:false,克隆创建了新实例
System.out.println(instance1 == instance2);
}
}
解决办法:重写clone()方法,直接返回已有的单例实例,而非创建新实例:
1
2
3
4
@Override
protected Object clone() throws CloneNotSupportedException {
return instance; // 返回已有单例,不创建新实例
}

4. 多线程环境下(懒汉式未正确同步)

原因:简单的懒汉式单例未加锁或锁粒度不当,多线程并发调用getInstance()时,可能同时进入“实例为空”的判断逻辑,导致多个线程各自创建实例。 代码示例(破坏过程)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 未加锁的懒汉式单例(线程不安全)
class UnsafeLazySingleton {
private static UnsafeLazySingleton instance;

private UnsafeLazySingleton() {}

public static UnsafeLazySingleton getInstance() {
if (instance == null) {
// 多线程并发时,多个线程可能同时进入这里
instance = new UnsafeLazySingleton();
}
return instance;
}
}

// 测试多线程破坏
public class TestMultiThread {
public static void main(String[] args) {
// 启动10个线程,获取实例并打印哈希值
for (int i = 0; i < 10; i++) {
new Thread(() -> {
System.out.println(UnsafeLazySingleton.getInstance().hashCode());
}).start();
}
}
}
// 输出:会出现多个不同的哈希值,说明创建了多个实例
解决办法:使用双重检查锁(DCL)+ volatile 保证线程安全(推荐),或改用静态内部类/饿汉式实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class SafeLazySingleton {
// volatile防止指令重排,保证实例初始化完成后才被其他线程可见
private static volatile SafeLazySingleton instance;

private SafeLazySingleton() {}

public static SafeLazySingleton getInstance() {
// 第一层检查:避免每次获取实例都加锁
if (instance == null) {
// 加锁:保证同一时间只有一个线程进入
synchronized (SafeLazySingleton.class) {
// 第二层检查:防止多个线程等待锁后重复创建
if (instance == null) {
instance = new SafeLazySingleton();
}
}
}
return instance;
}
}

5. 多个类加载器加载同一个单例类

原因:Java中“类的唯一性”由“类加载器 + 全类名”共同决定,不同的类加载器加载同一个单例类时,会生成不同的Class对象,每个Class对象对应一个实例,导致多个单例。 典型场景:Web应用中,不同的Web模块(如WAR包)使用各自的类加载器,加载同一个单例类,会产生多个实例。 解决办法:指定统一的类加载器加载单例类(如使用JVM的系统类加载器),或通过上下文类加载器统一管理。

6. 多JVM/多进程环境

原因:单例模式的作用域是单个JVM进程,在分布式系统、多进程部署的场景下,每个JVM进程都会创建自己的单例实例,全局来看就有多个实例。 说明:这不是“单例被破坏”,而是单例的作用域限制,若需全局唯一,需借助分布式锁(如Redis、Zookeeper)或中心化存储来保证。

总结

  1. 核心破坏原因:反射/序列化/克隆是通过“绕过私有构造方法”创建新实例;多线程是“并发创建”;多类加载器是“类元信息不唯一”;多JVM是“单例作用域受限”。
  2. 关键解决思路:防反射需在构造方法加校验,防序列化需重写readResolve(),防克隆需重写clone(),多线程需用DCL+volatile,多类加载器需统一加载器,多JVM需分布式锁。
  3. 最佳实践:优先使用“静态内部类”或“枚举单例”(枚举天然防反射、序列化,是最安全的单例实现)。
⬆︎TOP