单例模式的几个问题(2)-DCL的缺陷
单例模式的几个问题(2)-DCL的缺陷
DCL模式的单例实现方式一直是各大教科书、老师、博客的推荐实现方式,但是在高并发过程中仍存在失败的可能性。首先先看一个使用双重检查的单例模式:
//双重检查 public class Singleton4 { private static Singleton4 singleton4 = null; private Singleton4(){} public static Singleton4 getInstance(){ if(singleton4 == null){ synchronized (Singleton4.class){ if(singleton4 == null) singleton4 = new Singleton4(); } } return singleton4; } public void doSomething(){ System.out.println(this.getClass().getName()); } }
双检锁机制的出现确实是解决了多线程并行中不会出现重复new对象,而且也实现了懒加载。这样就没问题了吗? 重点来了 ,由于 new Singleton4() 不是一个原子操作,实际执行的时候需要以下几个步骤:
1.memory=allocate();
2.ctorInstance(memory); //对象初始化
3.instance=memory; //设置instance指向刚被分配的内存;
执行代码时,为了提高性能,编译器和处理器常常会对指令进行重排序。重排序分为三种:编译器重排序,指令级并行重排序,内存系统重排序,实现优化,优化结果可能是
-
初始化 Singleton4 对象;
-
把 Singleton4 对象地址赋给instance变量;
-
也可能是这样
-
初始化一半Singleton4对象;
-
把Singleton4对象地址赋给instance变量;
-
初始化剩下的Singleton4对象;
-
如果是第二种情况,在多线程情况下第一个线程已经进入了 singleton4 = new Singleton4(); 正在初始化,此时已经将Singleton4对象的地址赋给instance变量,但是Singleton4对象仍为初始化完毕。第二个线程进入函数 singleton4 == null 为假,获取instance对象并返回,线程如果访问其中某些属性或者方法可能访问到其成员变量的不正确值。具体来说Singleton.getInstance().getSomeField()有可能返回someField的默认值0。如果程序行为正确的话,这应当是不可能发生的事,因为在构造函数里设置的someField的值不可能为0。
很显然只要使用jmm的某种限制就可以让上面的重排序不会发生,这种重排序限制就是volatile。
volatile有两种内存语义,一种是缓存一致性,另一种就是加屏障了。所谓缓存一致性就是当读volatile变量时,jmm会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。至于屏障类型有四种,StoreStore屏障,StoreLoad屏障,LoadLoad屏障,LoadStore屏障。简单来说,只要volatile变量于普通变量之间的重排序可能破坏volatile的内存语义,这种重排序就会被编译器重排序规则和处理器内存屏障插入策略禁止。
由于有了volatile的存在,Singleton4的赋值指令不会被优化到new Singleton4()中间去,这就保证了另外一个线程如果看到了Singleton4被赋值的时候,其指向的对象一定是被初始化完成的。
浙公网安备 33010602011771号