单例模式和双重校验

参考链接:https://www.jianshu.com/p/3a7c7a54ed0b

public class Singleton {
    public static volatile Singleton instance = null;
    private Singleton() {					    //设置private构造函数,如果设置为public,该类之外的其他类可以随意创建Singleton的对象
    }										  //加上private之后,其他类想要创建Singleton的对象只可以通过public getInstance()方法
    public static Singleton getInstance() {		 //假如多线程10个调用getInstance()方法
        if (instance == null) {				    //首先判断A线程进来判断instance是否为空,为空则new Singleton()
            synchronized (instance) {			//但是如果A、B同时调用getInstance()方法,则同时走判断if,然后假设A走到synchronized,B卡住
                if (instance == null) {			//A获取对象,出去,B走进来,判断是否为空,此时不为空,return
                    instance = new Singleton();  
                }
            }
        }
        return instance;
    }
}

单例模式,该模式的目的是确保对象唯一。

双重校验,判断两次instance是否为空,为什么要这么写?首先我们有了synchronized,已经可以保证每次调用的都是同一个已经被创建的instance对象,为什么10个线程要使用同一个instance对象呢?因为比如进行如下的操作,就需要多个线程使用到同一个对象

  • 应用程序的日志(资源共享):一般日志内容是共享操作,需要在后面不断写入内容所以通常单例设计。

  • 统计当前在线人数(网站计数器):用一个全局对象来记录。

单例模式的优点同样可以减少内存开销,尤其是频繁的创建和销毁实例。

解释了单例模式的使用场景和必要性,再来回答为什么要使用双重校验来保证单一,这里涉及到了合理安排线程提升性能的问题。我们都知道并行可以提高效率,而synchronized内置锁的出现,虽然保证了原子性安全问题,但是使得锁里的资源无法同时被2个以上线程使用,所以锁里的内容应当越少越好,同样访问锁的线程也应该尽量避免排队等待,尽管CPU的调度我们无法控制,但是在编写代码时还是应该尽量优化,假如下面这样写(懒汉式写法)

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

OK我可以告诉你也可以实现单例模式,因为每个线程都会在synchronized前等待,等上一个线程释放锁之后再进锁判断一下instance有没有被创建,有则再退出,其他线程再进锁,假如A持锁创建,BCDE排队等待,B进入退出,C进入退出,D进入退出。。10个线程进10次锁,有没有感觉很难受。那么假如使用双例模式在锁外加一个instance==null的判断,那么A进锁创建对象,BCDE等待(已经判断当前调用方法时instance为空,在锁前等待),在A释放,BCDE仍然需要进去锁,但是FGHIJ线程再调用方法时,发现instance不为空,那么不用进锁直接return。这就是为什么要使用双例校验,锁外加一次判断,避免进锁操作,当然假如10个线程同时调用方法,在判断instance为空后还是会一个一个进锁,看起来和不适用双例校验没差,那也得使用双例校验,毕竟线程调用的时间不确定,而且这种极端情况很少出现。

volatile的作用

在并发编程中谈及到的无非是可见性、有序性及原子性。而这里的Volatile只能够保证前两个性质,对于原子性还是不能保证的,只能通过锁的形式帮助他去解决原子性操作。

我们首先要理解对象实例化的步骤:

  1. 分配内存空间。

  2. 初始化对象。

  3. 将内存空间的地址赋值给对应的引用。

上面是正常情况下,对象实例化的步骤,但是由于操作系统方面的原因。上面的第二步可能与第三步进行对换,如果发生这种情况,那么此时拿到的对象也只是一个引用,对于后面的业务操作可能存在错误的发生。

如果不加volatile,如下,创建对象instance

public class Singleton {
    public static volatile Singleton instance = null;
    public Singleton{
    	dosomething();		//先new对象但是构造函数还没执行
    }
    public static Singleton getInstance() {
        instance = new Singleton();
        return instance;
    }
}

通过反编译可以看出,构造函数也是一个函数,可能出现先分配内存空间,再instance = new Singleton();把地址给引用instance ,我们常说new了一个对象,其实对象在内存中,我们是看不见的,instance 只是存了一个可以指向对象的地址。而如果先得到地址,地址里面构造函数还没有执行,也就是没有初始化对象,此时需要调用的东西并不存在。

整个过程分配内存空间(打地基划地界),初始化对象(建房子添家具),地址赋给引用(挂门牌),连房子都没有建好,直接通过门牌号去找房子里面的东西肯定报错,哪怕房子建好了,但是我在找的时候是空的肯定有问题的,就是个时间先后的问题,所以加了volatile之后就可以防止此类问题的发生。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值