一文弄清单例模式

单例模式介绍

一个类只允许创建一个实例的设计模式被称为单例模式

当一个类被反复实例化并重复使用时,为了节约内存空间,会用到这种模式

单例模式分为饿汉式懒汉式,下面一一介绍

饿汉式

所谓的饿汉式,顾名思义,很饿就迫不及待地想创建实例对象,而java中最早可以创建的时候是类加载的时候,类加载首先会初始化静态代码,那么饿汉式就应该这样写:

public class Singleton {
    // 类加载初始化静态代码的时候就创建实例
    private static Singleton instance = new Singleton();
    // 不让外界new对象,所以构造器私有化
    private Singleton(){}
    // 开放一个接口来让外界拿到唯一实例化的对象,加static是由于不能实例化对象,只能通过静态函数Singleton.getInstance()来获取
    public static Singleton getInstance(){return instance;}
}

懒汉式

懒汉,就是很懒,在用户需要的时候我再创建对象,初始版本v1版就这样写

public class Singleton {
    // 懒人模式,先不实例化,需要的时候才实例化,懒人不到最后一刻不想行动
    private static Singleton instance;
    // 不让外界new对象,所以构造器私有化
    private Singleton(){}
    // 开放一个接口来让外界拿到唯一实例化的对象
    public static Singleton getInstance(){
    	// 调用方法时才实例化对象
        if(instance == null){
        	instance = new Singleton();
        }
        return instance;
    }
}

这种方法存在一个问题,那就是在多线程环境下会创建两个实例
比如当实例还没有被从初始化时,两个线程A和B,A与B同时进入执行Singleton.getInstance()函数,执行if(instance == null),同时判断为真,导致初始化了两个实例对象,与单例模式产生矛盾。

根据多线程知识,我们可以加锁来解决这个问题,v2版来了:

public class Singleton {
    // 懒人模式,先不实例化,需要的时候才实例化,懒人不到最后一刻不想行动
    private static Singleton instance;
    // 不让外界new对象,所以构造器私有化
    private Singleton(){}
    // 开放一个接口来让外界拿到唯一实例化的对象
    public static Singleton getInstance(){
    	// 加锁,防止多线程安全问题
        synchronized (Singleton.class){
            if(instance == null){
                instance = new Singleton();
            }
        }
        return instance;
    }
}

加锁是解决了多线程安全问题,解决的是在没有创建对象的时候,防止多线程创建多个实例对象的问题。但是上面这样写会带来一个效率问题,就是每次执行Singleton.getInstance()函数时,都会加锁,如果我之前就创建好了对象,那其实就不用加锁了,所以上面这样写会带来效率问题,那么在加锁的外层再加一个判断空不就行了,该懒汉式写法也被称为:Double Check(双重校验) + Lock(加锁)

public class Singleton {
    // 先不实例化
    private static Singleton instance;
    // 不让外界new对象,所以构造器私有化
    private Singleton(){}
    // 开放一个接口来让外界拿到唯一实例化的对象
    public static Singleton getInstance(){
    	// 多加一层判断,如果对象已经创建好了,就不用加锁,直接返回就行了
        if(instance == null){
            synchronized (Singleton.class){
                if(instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

可能会有疑问,那就是既然外面判断instance是否为空的if,那就说明instance不为空?里面的if是不是可以去掉?
是不行的,因为这种想法只是考虑到单线程的情况,考虑多线程,假如进入外面的if之后,别的线程在这个时候创建了实例,那么instance就不为空了,里面的if就不成立了

就此我们解决了多线程安全+效率问题。但是还是有不省心的地方,那就是java的指令重排带来的问题

指令重排是什么意思呢?比如java中简单的一句 instance = new Singleton,会被编译器编译成如下JVM指令:
memory =allocate(); //1:分配对象的内存空间

ctorInstance(memory); //2:初始化对象

instance =memory; //3:设置instance指向刚分配的内存地址

其中2和3的顺序是可以调换的,也就是可以执行1->3->2,当线程A执行完1,3时,instance对象还未完成初始化,但已经不再指向null。此时如果线程B抢占到CPU资源,执行 if(instance == null)的结果会是false,从而返回一个没有初始化完成的instance对象。

使用volatile关键字可以防止指令重排序。使用volatile关键字修饰的变量,可以保证其指令执行的顺序与程序指明的顺序一致,不会发生顺序变换,这样在多线程环境下就不会发生异常了。

volatile还有第二个作用:使用volatile关键字修饰的变量,可以保证其内存可见性,即每一时刻线程读取到该变量的值都是内存中最新的那个值,线程每次操作该变量都需要先读取该变量。

public class Singleton {
    // 给instance对象加上volatile,防止指令重排
    private volatile static Singleton instance;
    // 不让外界new对象,所以构造器私有化
    private Singleton(){}
    // 开放一个接口来让外界拿到唯一实例化的对象
    public static Singleton getInstance(){
    	// 多加一层判断,如果对象已经创建好了,就不用加锁,直接返回就行了
        if(instance == null){
            synchronized (Singleton.class){
                if(instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

到此,单例模式基本上已讲完了,下面是一点拓展,增加知识面,让面试更有谈资,自行取之:

懒汉式拓展

还有一种巧妙的方法,就是使用静态内部类实现单例模式:

public class Singleton {
	// 建立一个静态内部类来实现单例模式
    private static class LazyHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton (){}
    // 当外界使用Singleton.getInstance()时,LazyHolder.INSTANCE静态内部类才会初始化,创建实例对象
    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }
}

静态内部类是私有的,外界不能调用,当外界第一次调用getInstance()方法时,return LazyHolder.INSTANCE;会初始化静态内部类仅一次,创建唯一的实例对象,并且静态内部类也可以避免线程安全问题

使用静态内部类和上面的双重验证都不能避免一个问题,那就是通过反射强行拿到对象

学习过反射知识的同学,应该还能够回忆起来,可以通过反射强行获取一个类的对象,即便你的构造器是私有的,来回忆回忆:

//获得构造器
Constructor con = Singleton.class.getDeclaredConstructor();
//强行设置私有构造器为可访问
con.setAccessible(true);
//构造两个不同的对象
Singleton singleton1 = (Singleton)con.newInstance();
Singleton singleton2 = (Singleton)con.newInstance();
//验证是否是不同对象,输出false
System.out.println(singleton1.equals(singleton2));

这样就打破了单例模式

解决方案就是使用枚举类型

public enum SingletonEnum {
    INSTANCE;
}

额,一行代码就解决了,java的enum语法糖会阻止反射强行获取

在《effective java》中说道,最佳的单例实现模式就是枚举模式。利用枚举的特性,让JVM来帮我们保证线程安全和单一实例的问题。除此之外,写法还特别简单。

参考资料

1.漫画:什么是单例模式
2.我给面试官讲解了单例模式后,他对我竖起了大拇指!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值