作者|雷哥来源|Java面试真题解析(ID:aimianshi666)转载请联系授权(微信ID:GG_Stone)当面试官问“单例模式为什么一定要加volatile?”的时候,他指的是为什么惰性模式下的私有变量要是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有什么用?1.volatile的作用主要有两个作用,一是解决内存可见性问题,二是防止指令重排序。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后,程序中的内存可见性问题就可以解决了。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后一个特性(防止指令重排序),从而避免多线程执行,因为指令重排序排序导致部分线程拿到一个没有完全实例化的对象,导致程序执行错误。
