Java线程/内存模型的缺陷和增强 - 编程入门网
但如果把上面的代码放到多线程环境下运行,那么就可能会出现问题。假设有2条线程,同时执行到了if(res == null),那么很有可能res被初始化2次,为了避免这样的Race Condition,得用synchronized关键字把上面的方法同步起来。代码如下:
代码2 Class Foo{ Private Resource res = null; Public synchronized Resource getResource() { If (res == null) res = new Resource(); return res; }}
现在Race Condition解决了,一切都很好。 N天过后,好学的你偶然看了一本Refactoring的魔书,深深为之打动,准备自己尝试这重构一些以前写过的程序,于是找到了上面这段代码。你已经不再是以前的Java菜鸟,深知synchronized过的方法在速度上要比未同步的方法慢上100倍,同时你也发现,只有第一次调用该方法的时候才需要同步,而一旦res初始化完成,同步完全没必要。所以你很快就把代码重构成了下面的样子: 代码3 Class Foo{ Private Resource res = null; Public Resource getResource() { If (res == null) { synchronized(this) { if(res == null) { res = new Resource(); } } } return res; }}
这种看起来很完美的优化技巧就是Double-Checked Locking。但是很遗憾,根据Java的语言规范,上面的代码是不可靠的。 Java线程/内存模型的缺陷和增强(3)时间:2007-11-05造成DCL失效的原因之一是编译器的优化会调整代码的次序。只要是在单个线程情况下执行结果是正确的,就可以认为编译器这样的“自作主张的调整代码次序”的行为是合法的。JLS在某些方面的规定比较自由,就是为了让JVM有更多余地进行代码优化以提高执行效率。而现在的CPU大多使用超流水线技术来加快代码执行速度,针对这样的CPU,编译器采取的代码优化的方法之一就是在调整某些代码的次序,尽可能保证在程序执行的时候不要让CPU的指令流水线断流,从而提高程序的执行速度。正是这样的代码调整会导致DCL的失效。为了进一步证明这个问题,引用一下《DCL Broken Declaration》文章中的例子: 设一行Java代码: Objects[i].reference = new Object(); 经过Symantec JIT编译器编译过以后,最终会变成如下汇编码在机器中执行: 0206106A mov eax,0F97E78h0206106F call 01F6B210 ;为Object申请内存空间 ; 返回值放在eax中02061074 mov dword ptr [ebp],eax ; EBP 中是objects[i].reference的地址 ; 将返回的空间地址放入其中 ; 此时Object尚未初始化02061077 mov ecx,dword ptr [eax] ; dereference eax所指向的内容 ; 获得新创建对象的起始地址02061079 mov dword ptr [ecx],100h ; 下面4行是内联的构造函数0206107F mov dword ptr [ecx+4],200h02061086 mov dword ptr [ecx+8],400h0206108D mov dword ptr [ecx+0Ch],0F84030h
可见,Object构造函数尚未调用,但是已经能够通过objects[i].reference获得Object对象实例的引用。 如果把代码放到多线程环境下运行,某线程在执行到该行代码的时候JVM或者操作系统进行了一次线程切换,其他线程显然会发现msg对象已经不为空,导致Lazy load的判断语句if(objects[i].reference == null)不成立。线程认为对象已经建立成功,随之可能会使用对象的成员变量或者调用该对象实例的方法,最终导致不可预测的错误。 原因之二是在共享内存的SMP机上,每个CPU有自己的Cache和寄存器,共享同一个系统内存。所以CPU可能会 |
凌众科技专业提供服务器租用、服务器托管、企业邮局、虚拟主机等服务,公司网站:http://www.lingzhong.cn 为了给广大客户了解更多的技术信息,本技术文章收集来源于网络,凌众科技尊重文章作者的版权,如果有涉及你的版权有必要删除你的文章,请和我们联系。以上信息与文章正文是不可分割的一部分,如果您要转载本文章,请保留以上信息,谢谢! |