有关双检测锁定DCL和Singleton模式的问题•看OOP教材时,提到了一个双检测锁定(Double-CheckedLock,DCL)的问题,但是书上没有多介绍,只是说这是一个和底层内存机制有关的漏洞。查阅了下相关资料,对这个问题大致有了点了解。•从头开始说吧。•在多线程的情况下Singleton模式会遇到不少问题,一个简单的例子•1:classSingleton{•2:privatestaticSingletoninstance=null;•3:•4:publicstaticSingletoninstance(){•5:if(instance==null){•6:instance=newSingleton();•7:}•8:returninstance;•9:}•10:}•假设这样一个场景,有两个线程调用Singleton.instance(),首先线程一判断instance是否等于null,判断完后一瞬间虚拟机把线程二调度为运行线程,线程二再次判断instance是否为null,然后创建一个Singleton实例,线程二的时间片用完后,线程一被唤醒,接下来它执行的代码依然是instance=newSingleton();•两次调用返回了不同的对象,出现问题了。•最简单的方法自然是在类被载入时就初始化这个对象:privatestaticSingletoninstance=newSingleton();•JLS(JavaLanguageSpecification)中规定了一个类只会被初始化一次,所以这样做肯定是没问题的。•但是如果要实现延迟初始化(Lazyinitialization),比如这个实例初始化时的参数要在运行期才能确定,应该怎么做呢?•依然有最简单的方法:使用synchronized关键字修饰初始化方法:•1.publicsynchronizedstaticSingletoninstance(){•2.if(instance==null){•3.instance=newSingleton();•4.}•5.returninstance;•6.}•这里有一个性能问题:多个线程同时访问这个方法时,会因为同步而导致每次只有一个线程运行,影响程序性能。而事实上初始化完毕后只需要简单的返回instance的引用就行了。•双检测锁定解决方案•DCL是一个“看似”有效的解决方法,先把对应代码放上来吧:•1:classSingleton{•2:privatestaticSingletoninstance=null;•3:•4:publicstaticSingletoninstance(){•5:if(instance==null){•6:synchronized(this){•7:if(instance==null)•8:instance=newSingleton();•9:}•10:}•11:returninstance;•12:}•13:}•用JavaWorld上对应文章的标题来评论这种做法就是smart,butbroken。来看原因:•Java编译器为了提高程序性能会进行指令调度,CPU在执行指令时同样出于性能会乱序执行(至少现在用的大多数通用处理器都是out-of-order的),另外cache的存在也会改变数据回写内存时的顺序[2]。JMM(JavaMemoryModel,见[1])指出所有的这些优化都是允许的,只要运行结果和严格按顺序执行所得的结果一样即可。•Java假设每个线程都跑在自己的处理器上,享有自己的内存,和共享的主存交互。注意即使在单核上这种模型也是有意义的,考虑到cache和寄存器会保存部分临时变量。理论上每个线程修改自己的内存后,必须立即更新对应的主存内容。但是Java设计师们认为这种约束会影响程序性能,他们试着创造了一套让程序跑得更快、但又保证线程之间的交互与预期一致的内存模型。•synchronized关键字便是其中一把利器。事实上,synchronized块的实现和Linux中的信号量(semaphore)还是有区别的,前者过程中锁的获得和释放都会都会引发一次MemoryBarrier来强制线程本地内存和主存之间的同步。通过这个机制,Java中的同步机制保证了synchronized块中指令的原子性(atomic)。•双检测锁定的问题•好了,回过头来看DCL问题。看起来访问一个未同步的instance字段不会产生什么问题,我们再次来假设一个场景:•线程一进入同步块,执行instance=newSingleton();线程二刚开始执行getResource();•按照顺序的话,接下来应该执行的步骤是1)分配新的Singleton对象的内存2)调用Singleton的构造器,初始化成员字段3)instance被赋为指向新的对象的引用。•前面说过,编译器或处理器都为了提高性能都有可能进行指令的乱序执行,线程一的真正执行步骤可能是1)分配内存2)instance指向新对象3)初始化新实例。如果线程二在2完成后3执行前被唤醒,它看到了一个不为null的instance,跳出方法体走了,带着一个还没初始化的Singleton对象。•错误发生的一种情形...