单例模式的实现方式有很多种,饿汉模式、懒人模式、静态内部类和枚举等等,当面试官问到“为什么单例模式一定要加volatile?”时,他指的是最重要的是lazy模式下私有变量为什么要volatile?懒人模式指的是对象创建的懒加载方式。程序启动时并没有创建对象,而是在真正第一次使用对象时才创建对象。想解释一下为什么要加volatile?先来看懒人模式的具体实现代码:publicclassSingleton{//1.防止外部直接new对象破坏单例模式privateSingleton(){}//2.通过私有变量保存单例对象【新增volatile修饰】privatestaticvolatileSingletoninstance=null;//3.提供一个获取单例对象的公共方法publicstaticSingletongetInstance(){if(instance==null){//第一个效果synchronized(Singleton.class){if(instance==null){//第二个实例instance=newSingleton();}}}返回实例;}}从上面的代码我们可以看出,为了保证线程安全和高性能,代码中使用了两个if和synchronized来保证程序的执行。既然已经有synchronized保证线程安全,为什么要给变量加上volatile呢?在解释这个问题之前,我们首先要了解一个前置知识:volatile有什么用?一、volatile的作用Volatile主要有两个作用,一是解决内存可见性问题,二是防止指令重排序。1.1内存可见性问题所谓内存可见性问题是指多个线程同时操作一个变量。一个线程修改变量的值后,其他线程无法感知变量的修改。这就是内存可见性问题。使用volatile可以解决内存可见性的问题,比如下面的代码,不加volatile时,其实现如下:privatestaticbooleanflag=false;publicstaticvoidmain(String[]args){Threadt1=newThread(newRunnable(){@Overridepublicvoidrun(){//如果标志变量为真,执行将终止while(!flag){}System.out.println("终止执行");}});t1.开始();//1s后,修改flag变量的值为trueThreadt2=newThread(newRunnable(){@Overridepublicvoidrun(){try{Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}System.out.println("设置标志变量的值为真!");flag=true;}});t2.start();}上面程序的执行结果如下:但是,上面的程序在执行了N久之后,仍然没有结束执行,也就是说线程2修改了标志变量后,线程1根本感知不到变量的修改。那么,接下来,我们尝试给flag加上volatile,实现代码如下:publicclassvolatileTest{privatestaticvolatilebooleanflag=false;publicstaticvoidmain(String[]args){Threadt1=newThread(newRunnable(){@Overridepublicvoidrun(){//如果标志变量为真,终止执行while(!flag){}System.out.println("终止执行");}});t1.开始();//1s然后修改标志变量的值为真Threadt2=newThread(newRunnable(){@Overridepublicvoidrun(){try{Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}System.out.println("将标志变量的值设为真!");flag=true;}});t2.开始();}}以上程序的执行结果如下:从上面的执行结果可以看出,使用volatile后,程序中的内存可见性问题就可以解决了。1.2防止指令重排序指令重排序是指在程序执行过程中,编译器或JVM经常对指令进行重排序以提高程序执行性能。指令重排序的初衷真的很好,在单线程中也能发挥很大的作用。但是,在多线程中,使用指令重排序可能会导致线程安全问题。所谓线程安全问题,就是指程序的执行结果,与我们的预期不符。比如我们期望的正确结果是0,但是程序的执行结果是1,那么这就是线程安全问题。使用volatile可以禁止指令重排序,从而保证程序在多线程时能够正确执行。2、为什么要用volatile?回到正题,我们在单例模式下使用volatile,主要是因为使用volatile可以禁止指令重排序,从而保证程序的正常运行。这里可能有读者会问,不是已经用synchronized来保证线程安全了吗?那为什么要加volatile呢?看下面代码:publicclassSingleton{privateSingleton(){}//使用volatile禁止指令重排序privatestaticvolatileSingletoninstance=null;publicstaticSingletongetInstance(){if(instance==null){//①synchronized(Singleton.class){if(instance==null){instance=newSingleton();//②}}}返回实例;}}注意上面的代码,我在①和②代码处标出了两行。在私有变量中加入volatile主要是为了防止在执行②,即“instance=newSingleton()”的执行过程中指令重新排序。这行代码看似只是一个创建对象的过程,但它的实际执行却分为以下3个步骤:创建内存空间。在内存空间中初始化对象Singleton。将内存地址赋给实例对象(执行这一步后,实例不等于null)。试想一下,如果不加volatile,那么线程1执行到上述代码的②点时,可能会进行指令重排序,将原来的1,2,3的执行顺序重新排序为1,3,2。但是,在特殊情况下情况下,线程1执行完步骤3后,如果线程2执行到上述代码的①点,判断实例对象不为null,但是线程1还没有实例化该对象。那么线程2会得到一个实例化的“半个”对象,这会导致程序执行错误,这就是为什么要在私有变量中加入volatile的原因。综上所述,使用volatile可以解决内存可见性问题,防止指令重排序。我们在单例模式下使用volatile主要是为了利用volatile的后一个特性(防止指令重排序),从而避免多线程执行,因为指令重排序排序导致部分线程拿到的是一个没有完全实例化的对象,导致程序执行错误。判断是非在自己,名誉在别人,得失在人数。公众号:Java面试真题分析面试合集:https://gitee.com/mydb/interview
