面试一直在问的单例模式之双重校验

       单例模式使用非常广泛,一般都是一个单例工厂,直接去获取对象,最简单的当然是直接新建一个对象直接返回(new Single()),不过这种的话不太友好哦,要是这样,那还不如直接新建,要啥子单例模式,单利工厂啊。单例模式这个设计模式功能单一,但是涉及到的知识点还是比较多的,比如并发、锁、对象创建底层等等。下面我们来好好唠唠。

     先说下面试经常会问的吧,对性能最友好的单例模式,就是尽量不创建新的对象。先上代码吧。

/**
 * 单例模式
 */
public class Single {
    /**
     * 构造方法私有化,不然搞啥单例模式啊,人家直接new出来了。
     */
    private Single() {
    }

    /**
     * 定义一个静态常量
     */
    private static volatile Single instance = null;

    /**
     * 既然没有构造方法,你肯定要提供一个静态方法来获取类对象啊,不然获取不到对象,直接GG,哈哈
     * @return
     */
    public static Single getInstance(){
//        return new Single();   //这种和Single s = new Single()没啥子区别啊,费力不讨好

        if(instance == null){     //首先判断是否为空,不为空直接返回
            synchronized (Single.class){ 
          //并发情况下,多个线程已经完成了为空判断,继续执行,需要加锁,防止都生成新的Single
          //再次判断,上面说了,多个线程已经完成了为空判断,比如,线程1,2 都完成instance==null的判断,1生成了new Single
          //并返回,2线程继续运行,进入synchronized中的代码,这个时候instance已经不为空
了,但是线程2刚刚判断的instance是空的
                //所以还要再次判断
                if (instance==null){
                    instance = new Single();  //新建对象
                }
            }
        }
        return instance;
    }
}

        代码中一些需要讲解的地方已经加了注释,大家应该都能看明白,下面我再唠嗑下注释中没有说到的哈。

       单例模式创建主要的思想就是构造方法私有化,然后建立一个静态方法获取到对象。双重校验模式干了这么多事情,无非也就是为了尽量不新建对象,我们来看下其中使用了那些机制。

       首先,加锁,单例模式肯定要考虑多线程并发,加锁就是为了在多线程并发下尽量只创建Single一次,使用的锁是synchronized,如果大家对这个加锁不太熟悉的话,可以去看看我之前写的一篇文章谈谈对java线程的理解(三)--------关键字synchronized

      双重判断是否为空,在代码注释中已经讲得很清楚了,我们来看下synchronized是否可以加到方法中?

public static synchronized Single getInstance(){
}

     这样也是加锁哦,看我之前写的文章,大伙应该都明白,如果synchronized加到方法中,因为这个是静态方法,其实是就是相当于锁住了整个类,只要调用这个方法就会加锁,不管instance是否为空,所以明显是不太友好的,双重校验是instance为空的时候才会加锁的,优劣一看就很明白。

          再之后就是知识点volatile了,为什么要加上这个关键字呢?双重校验已经很好了啊。
          说到这里,就要讲下java中对象的创建了。对象的创建一般有五步,加载类、分配内存空间、内存空间初始化(赋0值)、设置对象头信息、初始化对象。 对象的创建牵涉到不少东西,有兴趣的小伙伴可以去看看,我感觉自己把控的还不太好,过段时间看能不能写一篇哈。   


      下面我们来看下这行代码 instance = new Single();很简单也很基础,不过一切代码一旦碰上了高并发,啥问题都来了,哈哈。这行代码明显不是原子性的,涉及倒多个操作。我们来说说主要的三步操作,就是高并发情况下容易出错的。                       
     1.创建类,分配内存空间,就是初始化对象执行了创建类的前两步(一般资料里是说创建类两步:分配内存空间,初始化),这个时候对象已经是完整的了,只是没有初始化,还是null。                                             
     2.初始化对象,就是对对象赋值,不在是null。                                                            
     3.赋值就是“=”,                                      

                                     

             
         这一行代码instance = new Single()总共有这三个操作步骤,我们知道,Jvm中有个机制--指令重排,就是在不影响代码整体逻辑的情况下,Jvm会尽量优化性能,对一些指令进行重排序。如果步骤2和3操作顺序乱了,在return之前,就是在释放锁的那一瞬间,因为这个没有处理逻辑,只有一个释放锁的操作,是有可能进行指令重排的。这个时候instance还是null,那进来的线程判断instance==null,就是true了,这个时候又会新建一个Single了。  

                     

                                          
    所以为了防止指令重排,加上了一个关键字volatile。这个关键字的其中一个功能就是防止指令重排序。当然volatile还能保证原子化操作数据的安全性,就好比多个线程对int i=3(原子性的)进行赋值,比如线程一设置i=4,线程二设置i=5,线程一设置i=4之后线程等待,线程二设置成了5,那么线程一再去获取i的时候,i=5。当然,如果小伙伴不太了解高速缓存区的话,可能觉得我在废话,你都设置成5了,当然是5啦。其实每个线程都会存一些临时的数据到cpu的高速缓存区,这也是多线程数据不一致的主要原因,有兴趣的小伙伴可以深入下哈。      

                             

                                                       
    所以加上volatile,就防止了代码instance = new Single()的指令重排,保证在赋值之后,instance已经不为空了,那么之后线程在判断代码instance == null的时候,自然也就是false,也就会直接返回了。     

             双重校验单例模式中的知识点我大部分都讲到了,哪里讲得不对,大家可以在评论中提点下哈。

        排版看的我好心酸(苦笑~~)

       No sacrifice,no victory~

猜你喜欢

转载自blog.csdn.net/zsah2011/article/details/109059728