vlambda博客
学习文章列表

单例模式-双检锁就稳了?

前言

前两天和一位朋友聊到单例模式的双检锁,相信了解过线程并发的朋友对双检锁不陌生,也可以看我之前分享的文章 。

这位朋友问我:双检锁能解决反射攻击和序列化问题吗?说实话,正好碰到我的知识盲区了,恰逢今天同事也提到单例,简单了解一下相关知识。

双检锁

我们先来回顾一下双检锁的经典写法。

public class Singleton {

    private static volatile Singleton singleton;

    private Singleton() {}

    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

反射攻击

大家知道,反射是可以访问类中私有域的成员,即使构造方法声明为私有。我们验证一下想法。

    public static void main(String[] args) throws Exception{
        Singleton s1 = Singleton.getInstance();

        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton s2 = constructor.newInstance();

        System.out.println(s1.hashCode());
        System.out.println(s2.hashCode());

        System.out.println(s1 == s2);
    }

输出结果:

可以看到通过反射我们绕过了双检锁校验,直接创建了一个全新的对象。

解决方案其实很简单,既然反射最终也是需要通过构造方法创建对象,那么我们只需要在构造方法中加入相关校验即可。

public class Singleton {
    private static Singleton instance = null;
    private static int count = 0;

    private Singleton() {
        synchronized (Singleton.class) {
            // 确保只创建一个对象实例
            if(count > 0){
                throw new RuntimeException("创建了两个实例");
            }
            count++;
        }
    }

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

序列化

上面的反射攻击比较好理解,那为什么序列化也能破坏单例呢?做一下实验。

class Singleton implements Serializable {

    private static final long serialVersionUID = 1L;
    String name;

    private Singleton() {
    }

    private static Singleton instance = new Singleton();

    public static Singleton getInstance() {
        return instance;
    }

    public static void main(String[] args) throws Exception {
        Singleton s1 = null;
        Singleton s = Singleton.getInstance();

        FileOutputStream fos = null;
        ObjectOutputStream oos = null;

        FileInputStream fis = null;
        ObjectInputStream ois = null;
        // 将对象序列化到文件中
        try {
            fos = new FileOutputStream("Singleton.obj");
            oos = new ObjectOutputStream(fos);
            oos.writeObject(s);
        } finally {
            oos.flush();
            oos.close();
            fos.close();
        }

        // 将对象从文件当中反序列化
        try{
            fis = new FileInputStream("Singleton.obj");
            ois = new ObjectInputStream(fis);
            s1 = (Singleton) ois.readObject();
        }finally{
            ois.close();
            fis.close();
        }

        System.out.println(s.hashCode());
        System.out.println(s1.hashCode());

        System.out.println(s == s1);
    }

}

输出结果:

单例模式-双检锁就稳了?

可以看到序列化破坏了单例,反序列化的结果是另外一个对象。

总结

通过这篇文章,我们了解了:

  • 反射攻击破坏单例模式,以及应对方法

  • 序列化破坏单例

相信大家对序列化破坏单例还留有疑问,在下一篇文章中,我会提到相关原理以及应对方法。

如果觉得文章对你有帮助,欢迎留言点赞。




大家的关注是我持续输出的动力

长按扫码可关注