单例模式介绍

原文链接:单例模式介绍 – 编程屋

目录

1 前言

2 单例模式类型

2.1 饿汉式:

2.2 懒汉式:

2.2.1 双重检查锁

2.2.2 volatile防止指令重排

2.3 静态内部类

3 破坏单例


1 前言

单例模式是指在内存中有且只会创建一次对象的设计模式,在程序中多次使用同一个对象且作用相同的时候,为了防止频繁的创建对象,单例模式可以让程序在内存中创建一个对象,让所有的调用者都共享这一单例对象。单例模式的类型有两种:懒汉式饿汉式

2 单例模式类型

  • 饿汉式:在类加载的时候已经创建好该单例对象。
  • 懒汉式:在需要使用对象的时候才会去创建对象

2.1 饿汉式:

//饿汉式
public class Hungry {

    /**
     * 构造器私有 拒绝别人创建这个对象
     */
    private Hungry() {
    }

    private final static Hungry HUNGRY = new Hungry();

    public static Hungry getInstance(){
        return HUNGRY;
    }
}

大家都知道饿汉式单例是程序启动的时候就已经创建好了对象,那么这样的会会有什么问题呢?有可能浪费空间

为什么呢?因为如果在该类中声明了许多内存空间,但却没有使用的话,就很浪费内存空间,因为饿汉式它是在程序启动的时候就已经创建好了。如下:

//饿汉式
public class Hungry {

    //可能会浪费空间
    private byte[] data1 = new byte[1024 * 1024];
    private byte[] data2 = new byte[1024 * 1024];
    private byte[] data3 = new byte[1024 * 1024];
    private byte[] data4 = new byte[1024 * 1024];
    private byte[] data5 = new byte[1024 * 1024];
    
    /**
     * 构造器私有 拒绝别人创建这个对象
     */
    private Hungry() {
    }

    private final static Hungry HUNGRY = new Hungry();

    public static Hungry getInstance(){
        return HUNGRY;
    }
}

那么既然饿汉式单例有问题,那么就出现了懒汉式单例模式,需要对象的时候才会去创建。

2.2 懒汉式:

//懒汉式单例
public class LazySingle {

    private LazySingle(){

    }
    private static LazySingle lazySingle;

    public static LazySingle getInstance(){
        if (lazySingle == null ) {
            lazySingle = new LazySingle();
        }
        return lazySingle;
    }
}

2.2.1 双重检查锁

但是这样会不会有问题呢?这样创建的懒汉式单例模式在单线程环境下肯定是没问题的,但是在多线程环境下,就会有问题了。就会不止创建一个对象了,那么如何改进呢?如下:

//懒汉式单例
public class LazySingle {

    private LazySingle(){
        System.out.println(Thread.currentThread().getName()+"ok");
    }
    private static LazySingle lazySingle;

    //双重检查加锁
    public static LazySingle getInstance(){
        if (lazySingle == null ){
            synchronized (LazySingle.class){
                if (lazySingle == null ) {
                    lazySingle = new LazySingle();
                }
            }
        }
        return lazySingle;
    }

这就是双重检查加锁的机制了(DCL懒汉式),这样就可以保证在多线程环境下有且仅会创建一个对象。

2.2.2 volatile防止指令重排

那么这样的双重检查锁是完整的吗,会不会出现一些其他的问题呢?其实也是会的,因为当我们在 new LazySingle()的时候,其实是有可能发生指令重排的。

  • 1 分配内存空间
  • 2 执行构造方法,初始化对象
  • 3 把这个对象指向这个空间

正常情况下,执行的这个顺序是1,2,3,如果在发生了指令重排,并且在多线程的环境下,也会出现问题。比如:A线程指令重排1,3,2,那么在重排过程中线程B进来,发现lazySingle已经分配内存空间了,不等于null了,那么就直接返回了,对于这种情况应该怎样处理呢?可以使用volatile关键字来解决,如下:

//懒汉式单例
public class LazySingle {

    private LazySingle(){
        System.out.println(Thread.currentThread().getName()+"ok");
    }
    private volatile static LazySingle lazySingle;

    //双重检查加锁
    public static LazySingle getInstance(){
        if (lazySingle == null ){
            synchronized (LazySingle.class){
                if (lazySingle == null ) {
                    lazySingle = new LazySingle();
                }
            }
        }
        return lazySingle;
    }


}

2.3 静态内部类

除此之外,我们还可以使用静态内部类来实现:

//静态内部类
public class Holder {

    private Holder(){

    }
    public static Holder getInstance(){
        return InnerClass.HOLDER;
    }

    public static class InnerClass{
        private static final Holder HOLDER = new Holder();
    }
}

3 破坏单例

但是无论是懒汉式单例还是饿汉式单例,都可以利用反射去破坏

    public static void main(String[] args) throws Exception {
        //通过正常方式获得对象
        LazySingle instance = LazySingle.getInstance();
        Constructor<LazySingle> declaredConstructor = LazySingle.class.getDeclaredConstructor(null);
        //可访问私有构造器
        declaredConstructor.setAccessible(true);
        //利用反射构造新对象
        LazySingle lazySingle = declaredConstructor.newInstance();
        System.out.println(instance.equals(lazySingle)); //false
    }

以上就是利用反射强制访问类的私有构造器,去创建另外一个对象

那么如何去解决它呢?

//懒汉式单例
public class LazySingle {

    private LazySingle(){
        synchronized (LazySingle.class){
            if (lazySingle != null ) {
                throw new RuntimeException("不要利用反射去破坏单例");
            }
        }
        System.out.println(Thread.currentThread().getName()+"ok");
    }
    private volatile static LazySingle lazySingle;

    //双重检查加锁
    public static LazySingle getInstance(){
        if (lazySingle == null ){
            synchronized (LazySingle.class){
                if (lazySingle == null ) {
                    lazySingle = new LazySingle();
                }
            }
        }
        return lazySingle;
    }


    public static void main(String[] args) throws Exception {
        //通过正常方式获得对象
        LazySingle instance = LazySingle.getInstance();
        Constructor<LazySingle> declaredConstructor = LazySingle.class.getDeclaredConstructor(null);
        //可访问私有构造器
        declaredConstructor.setAccessible(true);
        //利用反射构造新对象
        LazySingle lazySingle = declaredConstructor.newInstance();
        System.out.println(instance.equals(lazySingle));
    }
}

控制台输出:

 以上是通过正常调用了对象和通过反射调用了一次对象发现单例被破坏了,那么如果两次直接在反射中去创建对象,这样会被发现吗?

    public static void main(String[] args) throws Exception {
        //通过正常方式获得对象
//        LazySingle instance = LazySingle.getInstance();
        Constructor<LazySingle> declaredConstructor = LazySingle.class.getDeclaredConstructor(null);
        //可访问私有构造器
        declaredConstructor.setAccessible(true);
        //利用反射构造新对象
        LazySingle lazySingle = declaredConstructor.newInstance();
        LazySingle lazySingle1 = declaredConstructor.newInstance();
        System.out.println(lazySingle1.equals(lazySingle)); //false
    }
}

这样可见,直接这样去操作的话,单例被破坏依然是发现不了的,那么如何去解决呢?我们可以在私有构造器中加上一个标识,根据标识去判断,如下:

//懒汉式单例
public class LazySingle {

    private static Boolean flag = false;

    private LazySingle(){
        synchronized (LazySingle.class){
            if (!flag) {
                flag = true;
            }else {
                throw new RuntimeException("不要利用反射去破坏单例");
            }
        }
        System.out.println(Thread.currentThread().getName()+"ok");
    }
    private volatile static LazySingle lazySingle;

    //双重检查加锁
    public static LazySingle getInstance(){
        if (lazySingle == null ){
            synchronized (LazySingle.class){
                if (lazySingle == null ) {
                    lazySingle = new LazySingle();
                }
            }
        }
        return lazySingle;
    }


    public static void main(String[] args) throws Exception {
        //通过正常方式获得对象
//        LazySingle instance = LazySingle.getInstance();
        Constructor<LazySingle> declaredConstructor = LazySingle.class.getDeclaredConstructor(null);
        //可访问私有构造器
        declaredConstructor.setAccessible(true);
        //利用反射构造新对象
        LazySingle lazySingle = declaredConstructor.newInstance();
        LazySingle lazySingle1 = declaredConstructor.newInstance();
        System.out.println(lazySingle1.equals(lazySingle)); //false
    }
}

控制台输出:

这样就万无一失了吗?就能保证它不被破坏了吗,俗话说魔高一尺,道高一丈,反射依然可以去破坏它

    public static void main(String[] args) throws Exception {
        //通过正常方式获得对象
//        LazySingle instance = LazySingle.getInstance();
        Field flag = LazySingle.class.getDeclaredField("flag");
        flag.setAccessible(true);
        Constructor<LazySingle> declaredConstructor = LazySingle.class.getDeclaredConstructor(null);
        //可访问私有构造器
        declaredConstructor.setAccessible(true);
        //利用反射构造新对象
        LazySingle lazySingle = declaredConstructor.newInstance();
        flag.set(lazySingle,false);
        LazySingle lazySingle1 = declaredConstructor.newInstance();
        System.out.println(lazySingle1.equals(lazySingle)); //false
    }

 可见啊,无论我们如何去防止它,这个反射总是可以去破解我们的单例。那究竟怎样才能解决这个问题呢?那么就得看下反射到底是如何去创建一个对象的了。

发现显示不能用反射去破坏枚举,那么真的可以这样吗,我们来试一下:

枚举类:

public enum EnumSingle {

    INSTANCE;

    public EnumSingle getInstance(){
        return INSTANCE;
    }


}

 测试类:

class Test{
    public static void main(String[] args) throws Exception {
        EnumSingle instance1 = EnumSingle.INSTANCE;
        Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);
        declaredConstructor.setAccessible(true);
        EnumSingle instance2 = declaredConstructor.newInstance();

        System.out.println(instance1);
        System.out.println(instance2);
    }
}

测试结果:

可见真的不能用反射区破坏枚举。

注意:此时枚举类中不是无参构造,而是有参构造

private EnumSingle(String s,int i) {
    super(s,i);
}

以上只是部分内容,为了维护方便,本文已迁移到新地址:单例模式介绍 – 编程屋

  • 5
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
C++ 单例模式是一种常用的设计模式,它的主要目的是确保一个类只有一个实例,并提供全局访问该实例的方法。它通常被用来管理全局资源,例如日志记录器、配置文件、数据库连接等。 实现单例模式的关键在于确保类只有一个实例,并提供全局访问该实例的方法。在 C++ 中,可以通过静态成员变量和静态成员函数来实现这一点。 例如,下面是一个简单的 C++ 单例模式的实现: ```cpp class Singleton { private: static Singleton* instance; // 静态成员变量,用于保存单例实例 Singleton() {} // 构造函数私有化,保证外部无法直接创建实例 public: static Singleton* getInstance() { // 静态成员函数,用于获取单例实例 if (instance == nullptr) { instance = new Singleton(); // 如果实例不存在,就创建一个新的实例 } return instance; // 返回单例实例 } }; Singleton* Singleton::instance = nullptr; // 初始化静态成员变量 ``` 在上面的例子中,我们将构造函数私有化,这样就可以防止外部直接创建实例。同时,我们使用静态成员变量 `instance` 来保存单例实例,并使用静态成员函数 `getInstance` 来获取单例实例。在 `getInstance` 函数中,我们首先检查实例是否已经存在,如果不存在就创建一个新的实例。最后,我们返回单例实例。 使用单例模式时需要注意线程安全问题,可以使用线程安全的实现方式来避免多线程问题。此外,单例模式也有一些缺点,例如可能会导致代码耦合性增加、难以进行单元测试等。因此,在使用单例模式时需要谨慎考虑。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值