单例模式(懒汉模式和饿汉模式)

单例模式是一种创建型设计模式,它保证一个类只有一个实例,并提供一个全局访问点来访问该实例。在单例模式中,该类的构造函数被私有化,以防止其他对象创建该类的实例,同时提供一个静态方法来获取该类的唯一实例。

饿汉模式

在类加载时就创建出实例对象


//饿汉模式 在类加载时就创建好唯一的实例 只留下一个接口来获得这个实例的引用
class Singleton1 {
    //由于类的对象是单例,则类属性也是单例,则可以把类属性定义为单例的引用
    private static Singleton1 instance = new Singleton1();
    private Singleton1() {
    }
    public Singleton1 getInstance() {
        return instance;
    }
}

对类Singleton1进行封装,只留下一个getInstance方法来获取单例的引用,则在类加载时创建出的类变量instance 创建出了我们的单例。

懒汉模式

第一次使用单例时才创建

// 懒汉模式,在需要其实例时才会创建 会出现线程安全问题
class Singleton2 {
    private static volatile Singleton2 instance = null;
    private Singleton2() {

    }
    public Singleton2 getInstance() {
                //第一次使用则为null,就进行创建,以后直接返回第一次创建的这个实例
                if(instance == null){
                    instance = new Singleton2();
                }
        return instance;
    }
}

这只是我们基于单例和懒汉模式的要求写的出版本代码,实际上它存在很多的线程安全问题需要解决。

懒汉模式下的线程安全问题

1:原子性导致的问题

我们在第一次使用单例时,对instance进行是否为空的判断并创建单例:

if(instance == null){
         instance = new Singleton2();
                }

这包括了俩步:

1:判断instance是否为null

2:去cpu中创建类的实例对象

假设有如下情况:  

线程1判定null为空去cpu中创建类的实例对象,但是还没有创建完毕,此时instance在内存里仍然为null  。这个适合线程2也要来获得单例的引用,执行getInstance方法,此时由于instance为null,故线程2也去cpu创建类的实例对象。

如图所示,发生了俩次实例对象的创建,违背了单例的要求。

解决方法:synchronized加锁保证原子性:
synchronized (Singleton2.class) {
                if(instance == null){
                    instance = new Singleton2();
                }
            }

但是,synchronized加锁是一个成本很高的操作,它会产生阻塞等待,死锁,等问题,降低代码运行效率,所以我们不应该加没必要的锁。

在上述解决原子性问题当中,实际上只有第一次调用getInstance时才会产生原子性问题导致的线程安全问题,而后面的访问实际上是安全的。但上述代码不分青红皂白给所有的访问操作都加上了锁,大大影响到运行效率。

解决方法:套一个if循环来判断这一步是否需要加锁:

 if(instance == null) {
            synchronized (Singleton2.class) {
                if(instance == null){
                    instance = new Singleton2();
                }
            }
        }

2:内存可见性导致的问题

当多个线程同时对我们的instance变量进行访问时,我们不得不怀疑 cpu 是否有可能对我们的访问操作产生优化:直接将instance的值拿到寄存器上,每一次访问直接从寄存器上拿到instance的值,不再去内存上取了。  这样就会造成一个结果: 线程在创建出类的实例化对象后内存里的instance值仍然为空,导致多个实例对象创建出来。

解决方法:给instance变量加让volatile关键字,保证其内存可见性

private static Singleton1 instance = new Singleton1();

注意:这里只是分析了一下这种可能性,具体编译器是否会进行这样的优化不得而知,只是我们不能放过每一种可能造成线程安全问题的可能,加上volatile是更为稳妥的做法。

3:代码顺序性导致的问题

基于对内存可见性问题的分析,可以联想到另外一种造成线程安全问题的可能:指令重排序。

这也是编译器自动完成的优化,在进行实例化时,

正常的步骤是:1.申请空间

                         2.在空间中构造实例化对象

                         3.将内存地址赋值给instance引用

编译器有可能基于自己的执行效率的考量进行优化:

                        1:申请空间

                        2.将内存地址赋值给instance引用

                        3.在空间中构造实例化对象

在单线程当中,这种优化不会产生问题,但在多线程中就会产生线程安全问题:

线程1执行实例化操作,刚进行完上述步骤2:引用刚刚赋给变量,这时线程2访问这个变量,拿到了一个在内存中没有被构造完成的引用,这必然会引起问题。

解决方法:同内存可见性的解决方法一样:给变量加volatile关键字

最终版本:

class Singleton2 {
    private static volatile Singleton2 instance = null;
    private Singleton2() {

    }
    public Singleton2 getInstance() {
        if(instance == null) {
            synchronized (Singleton2.class) {
                if(instance == null){
                    instance = new Singleton2();
                }
            }
        }
        return instance;
    }
}

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值