设计模式———单例模式

单例也就是只能有一个实例,即只创建一个实例对象,不能有多个。

可能会疑惑,那我写代码的时候注意点,只new一次不就得了。理论上是可以的,但在实际中很难实现,因为你无法预料到后面是否会脑抽一下~~因此我们就可以通过编码技巧,让编译器来帮助我们检查约束。

单例模式又分为两种:饿汉式和懒汉式区别在于实例对象创建的时机不同

0x00 饿汉式

说到饿汉,那一定是很饥饿的人,很急切,也就是说这个实例对象创建的时机比较早,在类加载的时候就创建了。

要点

  • 1、由于创建时机较早,设置为静态即可在类加载的时候就可以创建实例对象

  • 2、由于只能创建一个实例对象,因此将构造函数私有化

  • 3、对外提供一个获取实例对象的静态类方法

    public class Singleton{
        private static Singleton instance = new Singleton();
        
        public static Singleton getInstance(){
            return instance;
        }
         private Singleton(){
             
         }
    }

0x01 懒汉式

懒汉,顾名思义,很懒,比较缓慢,也就是说这个实例对象是在使用到的时候才去创建。

要点

  • 1、一开始设置为null,直到在使用的时候再去new实例对象

  • 2、由于只能创建一个实例对象,因此将构造函数私有化

3、对外提供一个获取实例对象的静态类方法

public class SingletonLazy {
    private static SingletonLazy instance = null;

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

    private SingletonLazy(){

    }
}

0x02 单例模式与多线程

众所周知,许多在单线程环境下的代码一到多线程环境下就会出错,在此将对单例模式在多线程下的编写进行调整优化~

先来看看饿汉式的代码,我们发现这个实现的方式是在类加载的时候就去实例化对象了,而后续在多线程中掉用getInstance方法来获取实例对象的时候,只会进行返回操作,即”只读”因此多个线程在操作的时候是安全的

再来看看懒汉式,可以发现在调用getInstance方法的时候,会进行修改操作,如果恰巧有两个线程同时进入了if,然后就会进行new操作,那不就创建了多个实例对象了,如果恰巧每个对象不是很轻量,可能有很多属性,加载了几十G的数据到内存中......

因此解决方法是进行加锁操作,保证if和new操作是原子的

public class SingletonLazy {
    private static SingletonLazy instance = null;

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

    private SingletonLazy(){

    }
}

这样修改以后,就能保证if和new操作是一个整体,此时线程的安全问题就得到了改善。

但是这样写是否就真的完了呢?

答案:不是。因为我们是单例模式,所以只需要new一次之后,实例对象就不可能是null了,后续直接return对象就行了。假如我们现在已经创建好了一个对象,当后续有多个线程去调用getInstance方法的时候,需要去获取到这把锁才能进行返回操作,也就是我们的多线程变成了串行化,并发程度几乎没了~

那是否有办法,既可以保证代码线程安全,又不会对执行效率产生大的影响呢?

如下代码:

public class SingletonLazy {
    private static SingletonLazy instance = null;

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

    private SingletonLazy(){

    }
}

与前面的代码对比,可以发现,在外层多加了一个if语句。

分析

当有多个线程进入了第一个if语句,此时里面有一个加锁操作,这样可以保证只创建了一个实例对象。假设此时已经创建好了一个实例对象了,当下一次又有多个线程进来时,由于instance不为null,直接返回了,并不会进入到加锁操作,此时并发并没有再受到影响了,既保证了正确又保证了效率。

总结

在锁外面的if语句保证了是否要进行加锁操作,即如果还没有创建实例对象,此时线程由于需要进行修改操作要进行加锁才能保证安全,如果已经创建了实例对象,此时线程安全了就不用加锁了。

在锁里面的if语句是来保证只创建一个实例对象。

0x03 指令重排序

上述代码其实还不够安全,还存在指令重排序的问题。

指令重排序,本质是编译器为了提高执行效率,调整原有的代码执行顺序(前提是保持逻辑不变)。

接下来具体问题具体分析:

上述的代码中,出现了一个new操作,而new操作其实是分三步的,第一步,申请内存空间,第二步,在内存空间上构造对象,第三步,把内存地址赋值给对象引用。一般是按照1,2,3的顺序执行的,但是编译器优化过后,可能会按照1,3,2的顺序执行,在单线程环境下都可以,但是在多线程环境下则不是。如果线程1是按照1,3,2顺序执行完了3后,线程2此时返回了这个实例对象,但是此时这个对象还没有初始化呢,也就是说这是个非法对象,当我们的线程2去访问这个对象的属性或方法时,此时就是非法,会出错。此时我们还需要再对instance加一个限制操作,即用volatile来修饰instance。

最终代码

public class SingletonLazy {
    private static volatile SingletonLazy instance = null;

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

    private SingletonLazy(){

    }
}

至此,这段“健壮”的代码基本可以满足使用了~

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值