你的单例真的安全嘛?怎么破坏单例模式

492 阅读3分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

在之前的文章说,我们说过如何实现单例模式,那么今天我们就来看看怎么搞破坏,对应的,我们再来看看怎么弥补。

前情回顾

实现单例模式主要有三板斧。1 私有化构造器 2 创建私有的实例对象 instance 3 提供共有的方法获得 instance

@ThreadSafe
public class SingletonExample2 {
    // 私有化构造器
    private SingletonExample2(){}
    // 提供一个实例
    private static SingletonExample2 instance = new SingletonExample2();
    // 提供共有的方法返回实例
    public static SingletonExample2 getInstance(){
        return instance;
    }
}
​

我们说这种方式是线程安全的,但是它只是线程安全的,并不是真正的安全,如果你真在是想破坏它,也是有可能的。

使用反射创建对象

我们虽然私有化了构造器,但是还是可以通过反射来创建对象,这就是这个单例的漏洞。

    public static void main(String[] args) throws Throwable {
        SingletonExample2 instance = SingletonExample2.getInstance();
        Class<?> clazz = Class.forName("com.kris.workingtimes.practice.SingletonExample2");
        Constructor<?> constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        SingletonExample2 instance1 = (SingletonExample2) constructor.newInstance();
        if(instance == instance1) {
            System.out.println("Singleton break failed!");
        }else {
            System.out.println("Singleton break succeed!");
        }
    }

我们通过私有化构造器来保证类只能被自己创建,但是我们又通过反射来使单例不安全,呃呃呃,就是这么神奇。既然知道了问题,那就看看怎么解决这个问题呢?

我们只需要在构造器中控制即可,保证构造器只会被调用一次,如果有人在已经有实例的情况下再次调用构造器,直接报错即可。

    // 私有化构造器
    private SingletonExample2(){
        if(instance != null) {
            throw new RuntimeException("No reflect please ...");
        }
    }

使用反序列化创建对象

要想实现这种破坏,还有一个前提,就是你的单例对象已经实现了序列话接口。。。

破坏的代码这么写。

    public static void main(String[] args) throws Exception {
        SingletonExample2 instance1 = SingletonExample2.getInstance();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("tempFile"));
        objectOutputStream.writeObject(instance1);
        File file = new File("tempFile");
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
        SingletonExample2 instance2 = (SingletonExample2) objectInputStream.readObject();
        if (instance1 == instance2) {
            System.out.println("Singleton break failed!");
        } else {
            System.out.println("Singleton break succeed!");
        }
    }

那么好,都知道了这个破坏的前提,解决起来也很容易了,直接不实现序列化接口搞定…… 开玩笑哈,这种方式可以是可以,怕就怕万一你就需要在网络中传输这个对象呢?

其实还有另外一招就是重写 readResolve 方法,这个方法就是在调用对象的 readObject 方法的底层调用的,所以我们只要在 readResolve 方法中返回我们定义好的对象就行。

    private Object readResolve() {
        return instance;
    }

使用 Unsafe 对象破坏单例对象

最后这个就厉害了,因为我也不知道怎么预防,你要是知道,欢迎讨论哈。

    public static void main(String[] args) throws Exception {
        Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafeField.setAccessible(true);
        Unsafe unsafeInstance = (Unsafe)theUnsafeField.get(null);
        SingletonExample2 instanceA = (SingletonExample2)unsafeInstance.allocateInstance(SingletonExample2.class);
        SingletonExample2 instanceB = (SingletonExample2)unsafeInstance.allocateInstance(SingletonExample2.class);
        System.out.println(instanceA.hashCode());
        System.out.println(instanceB.hashCode());
        System.out.println(instanceA == instanceB);
    }

你会发现,这他么根本就没有调用构造器,还搞什么飞机,实际上 Unsafe 不需要调用构造函数。因为 Unsafe 是使用 C++ 进行 JVM 底层控制,那请问这该怎么控制呢???

总结

以上就是三种破坏单例模式的模式了,但是但是,我想说,这些东西你会在项目中实现嘛,你不会,你写了单例你再写一个破坏单例,好厉害啊你。还有就是据我所知,单例模式在我们的日常开发的业务系统中使用的不多,倒是在一些框架中使用的比较多。