17

关于"如何破坏单例"我说了好几种方式,面试官却说:我其实随便问问,没想到...

 3 years ago
source link: http://developer.51cto.com/art/202005/617183.htm
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。是一种创建型设计模式。他的定义为:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

单例模式一般体现在类声明中,单例的类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

但是其实,单例并不是完完全全安全的,也是有可能被破坏的。

以下,是一次面试现场的还原,之所以会聊到这个话题,是因为面试官问了我很多关于单例模式的问题,我回答的还可以,之后面试官随口问了一句"单例绝对安全吗?",紧接着发生了如下对话:

Q:单例模式绝对安全吗? 

A:(这个问题我知道,别想难倒我)不一定的,其实单例也是有可能被破坏的?

Q:哦?怎么说?

A:单例模式其实是对外隐藏了构造函数,保证用户无法主动创建对象。但是实际上我们是有办法可以破坏他的。

Q:那你知道有什么办法可以破坏单例吗??

A:有一个比较简单的方式,那就是反射。

反射破坏单例

我们先来一个比较常见的单例模式:

import java.io.Serializable;  
/**  
 * 使用双重校验锁方式实现单例  
 */  
public class Singleton implements Serializable{  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
        if (singleton == null) {  
            synchronized (Singleton.class) {  
                if (singleton == null) {  
                    singleton = new Singleton();  
                }  
            }  
        }  
        return singleton;  
    }  
} 

这个单例模式提供了一个private类型的构造函数,正常情况下,我们无法直接调用对象的私有方法。但是反射技术给我们提供了一个后门。

如下代码,我们通过反射的方式获取到Singleton的构造函数,设置其访问权限,然后通过该方法创建一个新的对象:

import java.lang.reflect.Constructor;  
public class SingletonTest {  
    public static void main(String[] args) {  
        Singleton singleton = Singleton.getSingleton();  
        try {  
            Class<Singleton> singleClass = (Class<Singleton>)Class.forName("com.dev.interview.Singleton");  
            Constructor<Singleton> constructor = singleClass.getDeclaredConstructor(null);  
            constructor.setAccessible(true);  
            Singleton singletonByReflect = constructor.newInstance();  
            System.out.println("singleton : " + singleton);  
            System.out.println("singletonByReflect : " + singletonByReflect);  
            System.out.println("singleton == singletonByReflect : " + (singleton == singletonByReflect));  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  
} 

输出结果为:

singleton : com.dev.interview.Singleton@55d56113  
singletonByReflect : com.dev.interview.Singleton@148080bb  
singleton == singletonByReflect : false 

如上,通过发射的方式即可获取到一个新的单例对象,这就破坏了单例。

Q:那这种破坏单例的情况,有办法避免吗?

A:其实是可以的,只要我们在构造函数中加一些判断就行了。

如下方式,我们在Singleton的构造函数中增加如下代码:

private Singleton() {  
    if (singleton != null) {  
        throw new RuntimeException("Singleton constructor is called... ");  
    }  
} 

这样,在通过反射调用构造方法的时候,就会抛出异常:

Caused by: java.lang.RuntimeException: Singleton constructor is called... 

序列化破坏单例

Q:嗯嗯,挺不错的,那我们换个问题吧。

A:(这部分面试官在犹豫问我什么问题,我主动提醒了他一句)其实,除了反射可以破坏单例,还有一种其他方式也可以的。

Q:嗯,那你就说说还有什么方式吧 

A:其实通过序列化+反序列化的方式也是可以破坏单例的。

如以下代码,我们通过先将单例对象序列化后保存到临时文件中,然后再从临时文件中反序列化出来:

public class SingletonTest {  
    public static void main(String[] args) {  
        Singleton singleton = Singleton.getSingleton();  
        //Write Obj to file  
        ObjectOutputStream oos = null;  
        try {  
            oos = new ObjectOutputStream(new FileOutputStream("tempFile"));  
            oos.writeObject(singleton);  
            //Read Obj from file  
            File file = new File("tempFile");  
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));  
            Singleton singletonBySerialize = (Singleton)ois.readObject();  
            //判断是否是同一个对象 
            System.out.println("singleton : " + singleton);  
            System.out.println("singletonBySerialize : " + singletonBySerialize);  
            System.out.println("singleton == singletonBySerialize : " + (singleton == singletonBySerialize));  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  
} 

输出结果如下:

singleton : com.dev.interview.Singleton@617faa95  
singletonBySerialize : com.dev.interview.Singleton@5d76b067  
singleton == singletonBySerialize : false 

如上,通过先序列化再反序列化的方式,可获取到一个新的单例对象,这就破坏了单例。

因为在对象反序列化的过程中,序列化会通过反射调用无参数的构造方法创建一个新的对象,所以,通过反序列化也能破坏单例。

Q:那这种破坏单例的情况,也同样有办法避免吗?

A:当然也有了。只要修改下反序列化策略就好了。

只需要在Sinleton中增加readResolve方法,并在该方法中指定要返回的对象的生成策略几可以了。即序列化在Singleton类中增加以下代码即可:

private Object readResolve() { 
    return getSingleton();  
} 

Q:为什么增加readResolve就可以解决序列化破坏单例的问题了呢?

A:因为反序列化过程中,在反序列化执行过程中会执行到ObjectInputStream#readOrdinaryObject方法,这个方法会判断对象是否包含readResolve方法,如果包含的话会直接调用这个方法获得对象实例。

Q3qQreB.jpg!web

Q:那如果没有readResolve方法的话,反序列化的时候会怎么创建对象呢?

A:当然也是反射咯。

Q:那前面不是说使用反射的情况,直接在构造函数抛异常不就行了吗?

A:这个我还真试过,其实是不行的,反序列化使用的反射构造器和我们代码中使用反射的构造器不是同一个,反序列化用到的构造器并不会调用到我们对象中的构造函数…balabala…(我也不知道面试官听不听得懂,感觉是没听懂…)

Q:哦。OK吧,请问你什么时候可以来上班?

不久之后,我入职了这家公司,在一次和当初的面试官聊天的时候,他无意间和我说:当时我面试你的时候,关于单例的破坏那几个问题,其实最开始我只是随口一问,没想到你给我吹水了20分钟…当时我就觉得你这家伙是个可造之材。

【责任编辑:庞桂玉 TEL:(010)68476606】


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK