相信大部分同学在面试中都遇到过手写单例模式的话题,那么如何写出完美的单例是面试官需要深入研究的问题,因为严谨的单例模式可能直接决定As一个采访的结果,今天我们要说的是看似线程安全的double-checklock单例模式下可能出现的指令重排问题。双重检查锁单例模式乍一看,下面的单例模式没问题,加个同步锁保证线程安全。从表面上看,确实没有问题。当多个线程同时执行单例时,会出现JVM指令重排的问题,可能会导致某个线程获取的单例对象未初始化。publicclassSingle{privatestaticSinglesingle;privateSingle(){}publicstaticSinglegetInstance(){if(null==single){synchronized(Single.class){if(null==single){single=newSingle();}}}returnsingle;因果关系事实上,代码single=newSingle()不是原子的。从代码层面来说,确实是没问题,但是如果你了解JVM的指令,就会知道,在执行这段代码的时候,需要在JVM中执行三个。指令完成如下://1:分配对象的内存空间memory=allocate();//2:初始化对象ctorInstance(memory);//3:设置实例指向刚才的内存地址分配的实例=内存;看到上面的指令重排解释完了,我们再来回顾一下没有volatile修饰符的单例为什么会出问题。假设有两个线程A和B调用singleton方法,当A线程执行到single=newSingle()时,如果编译器和处理器对指令重新排序,指令重新排序后://1:allocateobjectsmemoryspacememory=allocate();//3:设置instance指向刚刚分配的内存地址,对象还没有初始化instance=memory;//2:初始化对象ctorInstance(memory);当A线程执行到第2步(3:设置instance指向刚刚分配的内存地址,此时对象还没有初始化)变量single指向内存地址后不再为null,此时B线程进入第一个if时,因为single不再是null,那么不会执行同步代码块,而是直接返回未初始化对象的变量single,这样会导致后面的代码报错。解决问题也说清楚了,下面看看如何解决这个问题。解决问题的关键在于volatile关键字,因为它的可见性:当写入一个被volatile修饰的变量时,JMM会将本地内存中的值刷新到主存中。当读取一个被volatile修饰的变量时,JMM会将本地内存设置为无效。在多线程环境中,一个线程对共享变量的操作对其他线程是不可见的。Java提供了volatile来保证可见性。当一个变量被volatile修饰时,就意味着线程本地内存失效了。当一个线程修改共享变量后,会立即更新到主存中,而当其他线程读取共享变量时,会直接从主存中读取。当然synchronize和Lock都可以保证可见性。synchronized和Lock可以保证同一时刻只有一个线程获取锁然后执行同步代码,在锁释放前对变量的修改会刷新到主存。修正后的单例与上面的单例进行比较。下面的单例在私有静态变量single前面加上修饰符volatile,防止JVM指令重排,从而解决了单例对象的成员变量可能没有初始化的问题。publicclassSingle{privatevolatilestaticSinglesingle;privateSingle(){}publicstaticSinglegetInstance(){if(null==single){synchronized(Single.class){if(null==single){single=newSingle();}}}returnssingle;}}
