单例的七种实现的优缺点解析

      大家可能在项目中都遇到过单例模式的运用,那么,单例模式究竟运用于什么情况呢?它都有哪些实现方式呢?各个方式都有什么优缺点呢?今天我就带大家来盘点一下,单例模式。

      其实,单例模式在我们的项目中运用到的地方很多,其运用的目的,那么,为什么要运用单例模式呢?我们都知道,在多线程并发去执行操作的时候,保证不了数据的一致性,这是我们JMM构造所致,跟我们的CPU不同,CPU有我们的MESI(缓存一致性协议)

cpu硬件方面,我们做了一些处理,保证一致性,但是,在jmm层面,我们并没有去做任何的处理,而这只是在缓存层面上去保证的(为了减少内存和CPU之间协作的时间差异,CPU的操作时间远远高于我们的内存,防止出现CPU一直等待和内存之间的交互,就出来了L1,L2,L3(三级缓存),具体了解CPU的一级二级三级缓存),在运行时,jmm的工作区和内存是可能在寄存器和高速缓存和内存里都存在,所以说

这样交叉的形式,会导致数据不一致的问题,那么这就产生了问题:多个线程要操作同一对象(比如我之我只想new出来一个对象,只对这一个对象的属性进行操作,多线程的时候,不能保证我仅仅只创建出来一个对象),要保证对象的唯一性,这个问题怎么解决呢?

 

上图就是我们JMM工作时的划分和和JVM内存区域划分

上图可见,用volatile保证了我们并发操作的三大特征的可见性(其余是原子性和有序性,以后会出文章专门讲解JMM和硬件之间关系)

当运行我们的java文件的时候,其实是这样一个流程

将class字节码文件变成我们的计算机可以执行的二进制文件(010101),拆分成多个任务,进行线程的分配,然后线程转化为内核线程,这也就是我们所说的用户态转为内核态,然后通过OS的内核交给我们的CPU去执行

总而言之,多个线程要操作同一对象,没有任何措施是保证不了对象的唯一性的

就这样单例模式就发扬光大了!通过Jvm和其他的一些特性保证了多线程操作一个实例对象的时候是安全的,下面我们来一步步演示这七种单例模式

1.饿汉模式

 

public class HungryDemo {

    private static HungryDemo hungryDemo = new HungryDemo();

    public HungryDemo getInstance(){
        return hungryDemo;
    }


}

上图就是我们的饿汉模式的代码,为什么叫饿汉模式呢

就是所谓的迫不及待的去加载我们的对象实例,就是在类加载的时候就把我们的对象实例加载出来,我们知道,在类加载的时候,首先会把静态资源初始化完毕放在我们的jvm公共区域(方法区),而且只会被加载一次,也就是说,这个对象只会被实例化一次,所以,这样我们就能根据jvm类加载的特性保证它的唯一性了。但是,它虽然能保证线程安全的,但是性能就会有所下载,假设,我要为100个类做单例,这样的话,在类加载的时候,就会把这个一百的类全部加载到方法区,就会影响我们的程序性能,类多了,就可能会产生我们的类爆炸事件(内存溢出),而且,有的类我们并不是在项目启动的时候就必须马上用到,所以呢,没有延迟加载,这种类好长时间不使用,影响性能,懒汉模式就出现了!

 

 

2.懒汉模式

public class LazyDemo {

    private static LazyDemo lazyDemo = null;

    public LazyDemo getInstance(){
        if (lazyDemo==null){
            lazyDemo= new LazyDemo();
        }
        return lazyDemo;
    }

}

这样的话,不就可以进行延迟加载了嘛?当使用的时候才进行初始化 ,可能有些同学还是不理解,为什么这么写就能保证延迟加载了呢?原因是:加载一个类时,其内部类不会同时被加载。一个类被加载,当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时发生

这下大家明白了吧?但是这样的话,就会有问题呢,这样的话,假设两个线程同时判断lazyDemo这个类没有进行初始化,这样的话,就会产生数据的不一致性,同时产生两个一样的实例,所以,这种懒汉模式,也不能保证我们的线程安全问题,接下来,加了synchronized关键字的懒汉模式就诞生了!

3.懒汉式+同步方法

public class LazyAndSych {

    private static LazyAndSych  lazyAndSych = null;

    public synchronized static LazyAndSych getInstance(){
        if (lazyAndSych==null){
                lazyAndSych = new LazyAndSych();
        }
        return lazyAndSych;
    }

}

通过这个关键字保证我们的方法执行的原子性,就算有并发的情况发生,也不会造成产生不同的实例的这种情况

但是呢,这还有个缺点,就是,即使这样可以进行懒加载和保证线程安全,但是,这个方法就是串行的执行了,只有一个完成了,下个线程才能执行这个方法,性能就会急剧下降,接下来呢,Dubbo-check的单例就出现了!

4.dubbo-check(双重检查机制)

public class DubboCheckLazyDemo {

    private static DubboCheckLazyDemo dubboCheckLazyDemo = null;

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

    public static void main(String[] args) {
        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                System.out.println(DubboCheckLazyDemo.getInstance());
            }).start();
        }
    }

}
通过同步代码块来防止串行化的发生,但是,为什么要加两个判断呢?不这样做的话,会有性能问题,即:发现依旧存在着性能问题,也就是说,只要DubboCheckLazyDemo方法被调用,那么就会执行同步这个操作,于是我们加个判断,当dubboCheckLazyDemo没有被实例化的时候,也就是需要去实例化的时候才去同步,这样的话就不必再去走同步方法了,这样的话,即保证了线程安全,性能又好,但是这,还有一个缺陷,我们知道,我们写的代码顺序和真正执行的代码顺序是不一样了,因为为了提高程序性能做了指令重排序,这就会有一个问题,假如我们在此对象构造器里面加了几行代码,可能由于重排序会把构造器里面执行的逻辑排在实例化对象完后,这样这些代码逻辑(比如在构造器里面new其他的对象),就不会执行,操作的时候,就会报空指针错误!

所以,这时候,我们出现了volatile+dubbo-check避免这个问题

5(重点).volatile+dubbo-check

public class volatileDubboCheck {

    private static volatile volatileDubboCheck  singleton = null;

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

    }
}

因为volatile比较霸道,强制让其前面和后面的变量不进行重排,按顺序执行,这样就能保证不会出现NPE(NullPonitException)的问题了

6.Holder(持有者)

public class Holder {

    private static class HolderTo{

        private static Holder holder= new Holder();

    }
    public Holder getInstace(){
        return HolderTo.holder;

    }
}

持有者,故名思意,就是把我的对象交给其他人去进行实例化

变量交给另外一个类去持有(静态内部类)

 

声明类的时候,成员变量中不声明实例变量,而放到内部静态类中, 静态内部变量在类初始化的时候执行<init>方法去实例化这个变量(JVM类加载机制),而这个方法是一个同步方法,所以说,用静态内部类的这种方法去做,是可以保证线程安全的

这个是一般我们经常会用的,这个方法没有什么太大的缺陷,我也经常使用这种方式去实例化对象,嘿嘿嘿!

7.ENUM(枚举单例)

public enum  EnumSingleton {
    A;

    public static EnumSingleton getInstance(){
        return A;
    }



}
先给大家举一个枚举的栗子,这样做的话,其实就可以保证它出现对象实例是线程安全的,为什么呢,因为,枚举里面声明的枚举都是常量!!记住是常量!而且A即代表EumSingleton这个对象的实例!!记住!!所以可以保证我们的线程安全,但是这样的话性能有点不太好,那好,我们来进行改造一下
public class   EnumSingletonAndLazy {

    private EnumSingletonAndLazy(){

    }

    //延迟加载
    private enum EnumHolder{
        INSTANCE;
        private EnumSingletonAndLazy instance = null;
        EnumHolder(){
            instance = new EnumSingletonAndLazy();
        }
        private EnumSingletonAndLazy getInstance(){
            return instance;
        }
    }

    public static EnumSingletonAndLazy getInstance(){
        return EnumHolder.INSTANCE.instance;
    }
}

我们这样是不是完成了延迟加载,把变量交给我们的内部类去初始化,只有我们在调用内部类的方法的时候才会进行加载,这样我们的 性能是不是就会显著提升?EffectJava这本书也推荐用枚举去完成单例 上面那个代码的逻辑大家应该能看明白吧(这个枚举初始化的时候就对instance进行new了,因为声明INSTANCE这个枚举的时候已经进行了实例就会调用初始化方法,上面已经强调过了,即声明INSTANCE时完成实例化),这种方法不知道你有没有试过呢?没试过的话,赶紧尝试一下吧!

今天的介绍就到这里,大家到现在应该清楚明白了吧?不懂我们还可以交流喔,QQ:936997192

加油!

 

 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值