为了账号安全,请及时绑定邮箱和手机立即绑定

设计模式(四)-创建型之单例模式(附破坏单例模式的几种方法及源码分析)

标签:
设计模式

之前整理过一篇有关单例模式的文章,《看完了这些,再也不怕面试官问什么是单例模式了》,上篇文章主要整理了单例模式的一些写法,这篇文章添加了如何破坏单例模式

1.什么是单例模式?

单例模式是指一个类在任何情况下都只有一个实例,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

2.单例模式的适用场景和优缺点

2.1单例模式的适用场景

  • 确保在任何情况下都只有一个实例

2.2单例模式的优点

  • 只有一个实例,减少内存开销
  • 唯一的访问对象方式,严格控制访问
  • 避免对资源多重占用

2.3单例模式的缺点

  • 没有接口,扩展困难

3.单例模式中的角色和类图

3.1单例模式中的角色

  • Singleton
    • 在单例模式中,只有Singleton这一个角色。Singleton角色中有一个返回唯一实例的static方法,该方法总是会返回同一个实例。

3.2单例模式类图

单例模式的类图很简单,只有一个类

4.代码实现

这里直接截取《看完了这些,再也不怕面试官问什么是单例模式了》文章中单例模式的写法。

4.1懒汉式

/**
 * 懒汉模式
 * 单例实例在第一次使用时进行创建
 */
public class SingletonExample1 {

    // 私有构造函数
    private SingletonExample1() {

    }

    // 单例对象
    private static SingletonExample1 instance = null;

    // 静态的工厂方法
    public static SingletonExample1 getInstance() {
        if (instance == null) {
            instance = new SingletonExample1();
        }
        return instance;
    }
}

4.2饿汉式

/**
 * 饿汉模式
 * 单例实例在类装载时进行创建
 */
public class SingletonExample2 {

    // 私有构造函数
    private SingletonExample2() {

    }

    // 单例对象
    private static SingletonExample2 instance = new SingletonExample2();

    // 静态的工厂方法
    public static SingletonExample2 getInstance() {
        return instance;
    }
}

4.3懒汉式(synchronized)

 * 懒汉模式
 * 单例实例在第一次使用时进行创建
 */
public class SingletonExample3 {

    // 私有构造函数
    private SingletonExample3() {

    }

    // 单例对象
    private static SingletonExample3 instance = null;

    // 静态的工厂方法
    public static synchronized SingletonExample3 getInstance() {
        if (instance == null) {
            instance = new SingletonExample3();
        }
        return instance;
    }
}

4.4懒汉式(双重锁)

/**
 * 懒汉模式 -》 双重同步锁单例模式
 * 单例实例在第一次使用时进行创建
 */
public class SingletonExample4 {

    // 私有构造函数
    private SingletonExample4() {

    }

    // 1、memory = allocate() 分配对象的内存空间
    // 2、ctorInstance() 初始化对象
    // 3、instance = memory 设置instance指向刚分配的内存

    // JVM和cpu优化,发生了指令重排

    // 1、memory = allocate() 分配对象的内存空间
    // 3、instance = memory 设置instance指向刚分配的内存
    // 2、ctorInstance() 初始化对象

    // 单例对象
    private static SingletonExample4 instance = null;

    // 静态的工厂方法
    public static SingletonExample4 getInstance() {
        if (instance == null) { // 双重检测机制        // B
            synchronized (SingletonExample4.class) { // 同步锁
                if (instance == null) {
                    instance = new SingletonExample4(); // A - 3
                }
            }
        }
        return instance;
    }
}

4.5懒汉式(双重同步锁)

A指令重排序,先执行了1,3分配完内存并赋值给了instance,还没进行初始化,线程B看到instance不为null,则将还未初始化的实例对象返回了

代码:

public class SingletonExample4 {
    private SingletonExample4() {
    }

    //1.memory=allocate() 分配对象的内存空间
    //2.ctorInstance() 初始化对象
    //3.instance = nemory 设置instance指向刚分配的内存

    // jvm和cpu优化发生了指令重排序

    //单例对象
    private static SingletonExample4 instance = null;
    //静态工厂方法
    public static SingletonExample4 getInstance() {
        if (instance == null) { //线程B执行到这里
            synchronized (SingletonExample4.class) {
                if (instance == null) {
                    instance = new SingletonExample4();//线程A执行到这里
                }
            }
        }
        return instance;
    }
}

正确的代码,使用volatile禁止指令重排:

public class SingletonExample4 {
    private SingletonExample4() {
    }

    //1.memory=allocate() 分配对象的内存空间
    //2.ctorInstance() 初始化对象
    //3.instance = nemory 设置instance指向刚分配的内存

    // jvm和cpu优化发生了指令重排序

    //单例对象
    private static volatile SingletonExample4 instance = null;
    //静态工厂方法
    public static SingletonExample4 getInstance() {
        if (instance == null) { //线程B执行到这里
            synchronized (SingletonExample4.class) {
                if (instance == null) {
                    instance = new SingletonExample4();//线程A执行到这里
                }
            }
        }
        return instance;
    }
}

4.6饿汉式(静态代码块)

静态代码块写单例模式一定要注意对象声明一定要写在静态代码块赋值的前面(注意写代码的顺序),否则获取到的值是null值

/**
 * 饿汉模式
 * 单例实例在类装载时进行创建
 */
public class SingletonExample6 {

    // 私有构造函数
    private SingletonExample6() {

    }

    // 单例对象
    private static SingletonExample6 instance = null;

    static {
        instance = new SingletonExample6();
    }

    // 静态的工厂方法
    public static SingletonExample6 getInstance() {
        return instance;
    }

    public static void main(String[] args) {
        System.out.println(getInstance().hashCode());
        System.out.println(getInstance().hashCode());
    }
}

4.7懒汉式(枚举)-推荐

推荐的方式,JVM保证这个方法绝对只调用一次,且是在被调用一次

/**
 * 枚举模式:最安全
 */
public class SingletonExample7 {

    public static SingletonExample7 getInstance() {
        return Singleton.INSTANCE.getInstance();
    }

    private enum Singleton {
        INSTANCE;

        private SingletonExample7 singleton;

        // JVM保证这个方法绝对只调用一次
        Singleton() {
            singleton = new SingletonExample7();
        }

        public SingletonExample7 getInstance() {
            return singleton;
        }
    }
}

一起来破坏单例模式吧

1.序列化破坏恶汉式单例模式

一个单例对象创建好之后,有时需要将对象序列化后写入磁盘,下次使用时再从磁盘中读取出来并反序列化,转换为内存对象。反序列化后的对象会重新创建,如果反序列化的目标对象为单例对象,这就违背了单例模式的初衷,破坏了单例模式。
第一步:单例对象实现序列化接口,这里以饿汉式为例

public class SingletonExample2 implements Serializable {

    // 私有构造函数
    private SingletonExample2() {

    }

    // 单例对象
    private static SingletonExample2 instance = new SingletonExample2();

    // 静态的工厂方法
    public static SingletonExample2 getInstance() {
        return instance;
    }
}

第二步:编写测试代码

public class SerializationSingletonTest {
    public static void main(String[] args) {
        // 序列化对象
        SerializationSingleton serializationSingleton1 = SerializationSingleton.getInstance();
        // 反序列化对象
        SerializationSingleton serializationSingleton2 = null;
        String serializationObjName = "SerializationSingleton.obj";
        try {
            // 序列化对象到硬盘
            FileOutputStream fileOutputStream = new FileOutputStream(serializationObjName);
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
            objectOutputStream.writeObject(serializationSingleton1);
            objectOutputStream.flush();
            objectOutputStream.close();

            // 反序列化对象到内存
            FileInputStream fileInputStream = new FileInputStream(serializationObjName);
            ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
            serializationSingleton2 = (SerializationSingleton) objectInputStream.readObject();
            objectInputStream.close();
            // 对比
            System.out.println(serializationSingleton1);
            System.out.println(serializationSingleton2);
            System.out.println(serializationSingleton1==serializationSingleton2);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

1.1测试结果


从结果可以看出,反序列化后获取的对象和手动创建的对象不一致,违背了单例模式的初衷,如何保证在序列化的情况下也能够实现单例模式呢?其实值需要增加readResolve()方法。优化后的代码:

public class SerializationSingleton implements Serializable {

    // 私有构造函数
    private SerializationSingleton() {

    }

    // 单例对象
    private static final SerializationSingleton instance = new SerializationSingleton();

    // 静态的工厂方法
    public static SerializationSingleton getInstance() {
        return instance;
    }
    
    // 添加readResolve,防止反序列化破坏单例模式
    private Object readResolve() {
        return instance;
    }
}

再次执行测试代码:

可以看到,反序列化得到的对象和手动创建的对象是同一个对象了。大家一定会像,这是什么原因呢?看上去优点神奇。让我们一起来看一下ObjectInputStream类的readObject()方法。

readObject0(false)方法中有checkResolve(readOrdinaryObject(unshared))

我们看一下readOrdinaryObject(unshared)中的源码

在源码中可以看到,首先调用ObjectStreamClassisInstantiable()的方法,判断是否包含构造函数,如果包含构造函数,则通过反射创建一个新的对象。
然后判断是否包含readResolve方法

如果readResolve方法存在,则通过反射调用readResolve方法获取对象

通过JDK源码可以发现,resolve()方法返回实例解决了单例模式被破坏的问题,通过源码可以发现反序列化过程中实际上对象又被实例化了一次,但新创建的对象没有返回,内存开销会增大。

序列化破坏枚举式单例模式

编写测试代码

public class EnumSingletonSerializationTest {
    public static void main(String[] args) {
        // 序列化对象
        EnumSingleton enumSingleton1 = EnumSingleton.getInstance();
        // 反序列化对象
        EnumSingleton enumSingleton2 = null;
        String serializationObjName = "SerializationSingleton.obj";
        try {
            // 序列化对象到硬盘
            FileOutputStream fileOutputStream = new FileOutputStream(serializationObjName);
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
            objectOutputStream.writeObject(enumSingleton1);
            objectOutputStream.flush();
            objectOutputStream.close();

            // 反序列化对象到内存
            FileInputStream fileInputStream = new FileInputStream(serializationObjName);
            ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
            enumSingleton2 = (EnumSingleton) objectInputStream.readObject();
            objectInputStream.close();
            // 对比
            System.out.println(enumSingleton1);
            System.out.println(enumSingleton2);
            System.out.println(enumSingleton1 ==enumSingleton2);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

测试结果


我们可以看到枚举类是不可以序列化的,有效的保护了单例模式被破坏。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消