你的单例模式真的写完整了吗?

本文将以"DCL"单例模式为例,从克隆、序列化/反序列化、反射三个角度介绍破坏单例模式及如何防范。单例模式的其他写法类同,请读者参照《单例模式还能这样写?》自行转化。

# 克隆

克隆是一种良性的单例模式方法。使用克隆破坏代理模式有一定的局限性,需要实现Colneable接口并且重写克隆方法。

public class Singleton implements Cloneable {
    private volatile static Singleton instance = null;
    private Singleton() {}

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    public static Singleton getInstance() {
        if(instance == null) {
            synchronized(Singleton.class) {
                if(instance == null)
                    instance =  new Singleton();
            }
        }
        return instance;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

测试

@Test
public void cloneTest() throws CloneNotSupportedException {
    Singleton instance = Singleton.getInstance();
    Singleton instance_clone = (Singleton) instance.clone();
    
    Assert.assertNotEquals(instance, instance_clone);
}
1
2
3
4
5
6
7

结果true,这意味着两个单例对象实例不一致,违反单例对象的类必须保证只有一个实例存在的原则。

解决方案:开发时强制规定禁止单例对象实现Colneable接口

# 序列化/反序列化

序列化/反序列化也能够破坏单例模式,需要实现Serializable接口。相比于克隆的方式,这种方案局限性小一些,有一些单例对象无法避免的要序列化。

public class Singleton implements Serializable {
    private volatile static Singleton instance = null;
    private static final long serialVersionUID = 972132953734L;
    private Singleton() {}

    public static Singleton getInstance() {
        if(instance == null) {
            synchronized(Singleton.class) {
                if(instance == null)
                    instance =  new Singleton();
            }
        }
        return instance;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

测试

@Test
public void serializableTest() throws Exception {
    Singleton instance = Singleton.getInstance();
    ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("temp"));
    outputStream.writeObject(instance);

    File obj = new File("temp");
    ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(obj));
    Singleton instance_serializable = (Singleton) inputStream.readObject();

    Assert.assertNotEquals(instance, instance_serializable);
}
1
2
3
4
5
6
7
8
9
10
11
12

结果:true,同样意味着两个单例对象实例不一致,违反单例对象的类必须保证只有一个实例存在的原则。

解决方案:通过阅读源码,序列化底层采用反射机制将二进制数据反序列化为对象,同时在反射时会检查实现了序列化接口的类是否包含readResolve(),如果包含则返回true,然后会调用readResolve方法。因此,在单例类中添加readResolve()即可:

private Object readResolve() {
    return instance;
}
1
2
3

# 反射

反射机制的存在是现如今Java如此深受欢迎的原因之一。反射机制可以在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。通过反射,同样可以破坏单例模式,而且这种破坏是低成本的。

public class Singleton {
    private volatile static Singleton instance = null;
    private Singleton() {}

    public static Singleton getInstance() {
        if(instance == null) {
            synchronized(Singleton.class) {
                if(instance == null)
                    instance =  new Singleton();
            }
        }
        return instance;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

测试

@Test
public void reflectTest() throws Exception {
    Singleton instance = Singleton.getInstance();
    Class<?> clazz = Class.forName("club.ardien.Singleton");
    Constructor<?> constructor = clazz.getDeclaredConstructor();
    constructor.setAccessible(true);
    Singleton instance_reflect = (Singleton) constructor.newInstance();

    Assert.assertNotEquals(instance, instance_reflect);
}
1
2
3
4
5
6
7
8
9
10

结果true,同样意味着两个单例对象实例不一致,违反单例对象的类必须保证只有一个实例存在的原则。

解决方案:通过反射调用setAccessible(true)来破环构造方法的可见性来达到创建新对象的目的。因此我们只需要在构造方法中抛出异常即可:

private Singleton() {
    if (instance != null) throw new RuntimeException("instance is not null");
}
1
2
3
上次更新: 2020/09/02, 15:09:00
最近更新
01
RabbitMQ简介
10-27
02
聊聊Java多态
10-21
03
JVM垃圾回收器
10-16
更多文章>