现在的位置: 首页 > 综合 > 正文

Java Memory Model引发的血案

2012年06月29日 ⁄ 综合 ⁄ 共 3479字 ⁄ 字号 评论关闭

好吧,我标题党了。

 

之前的blog

简单的提及过java Memory Model,不过这次却真的碰到了由于JMM的特性导致的错误。

背景是跑系统的压力测试的时候突然抛出了一个NullPointerException。这事挺奇怪的,因为已经跑了很长时间的压力测试,可这会才莫名的报了个错,于是找到对应的代码行,如下所示:

 

Thread A

 

Thread B

 

上面两部分代码分别有不同的thread执行。正常执行的话,这个NullPointerException是不可能抛出的,因为在Thread A的代码块中,调用this.notify()之前this.data已经被赋值了。唯一的解释是,Thread A的代码块的执行顺序被打乱了。根据JMM的规范,是不一定保证程序代码的执行顺序的。在这里,“this.data = new Data();”这条语句可能被放在synchronized() 之后执行,于是当Thread B从this.wait()处唤醒时,this.data还没有被赋值呢,自然就抛错了。

 

正确的代码也很简单,将“this.data = new Data();”这行放入synchronized() {....}里面:

 

Thread A

 

由于采用了synchronized关键字,可以保证synchronized中的代码的执行结果在其它线程进入synchronzied中之前能够被看见。在这里,就保证了当thread进入到 synchronized代码中时,Thread A中的“this.data = new Data();”已经执行完了。

 

-- END --

 

-------------------------- 华丽的分割线 --------------------------------


补充一下:(因为这篇被推荐了,为了不误人子弟...)


评论中,roamm

提到synchronzied可能会提供memory barrier的效果,目前为止这一点仍然没有一个定论。我把这个问题今天发在了SMTH的java版,不过似乎讨论也没有最终的结果。


这个问题可以简单的归纳为:

如果对于一段代码

A

B

C

存在乱序执行的可能: (比如,实际的执行顺序为 C -> B -> A)


那么,改造一下这段代码

A

synchronzied{B}

C

是否仍然存在乱序执行的可能 (比如 C -> synchronzied{B} -> A)


目前没有结论。(应该这么说,没有找到任何证据可以证明synchronized提供了类似于memory barrier的效果)

 

----------------------------------  华丽丽的分割线  --------------------------------------------------


这个问题基本上有答案了,重新梳理一下吧。

还是举例说明:

假设对于Test的某个实例,有两个thread分别执行task1(thread A)和task2 (thread B)。
由于两个thread之间没有任何的同步,所以可能出现多种执行情况。但更特别的是,由于JVM的优化,存在的乱序的可能,所以有可能出现:
thread A:     x = b -----------------------------------------> a = 1
thread B:     ----------->b =1 -------> y = a ---------------------
最后,x=0,y=0
关于这一点,就不多说了。


现在的问题是,如果改变一下代码,变成:


那么,是否还有可能出现:
thread A:     synchronized{x = b;} -------------------------------------------------------------> a = 1
thread B:     ----------------------------------------------> synchronized{b = 1;}--> y = a ----------


结论是不可能再出现了。



Java Concurrency in Practice的3.1.3中有这么一段:

When thread A executes a synchronized block, and subsequently thread B enters a synchronized block guarded by the same lock, the values of variables that were visible to A prior to releasing the lock are guaranteed to be visible to B upon acquiring thelock.

In other words, everything A did in or prior to a synchronized block
is visible to B when it executes a synchronized block guarded by the same lock. Without synchronization, there is no such guarantee.

.......

Locking is not just about mutual exclusion; it is also about memory visibility.


意思是,如果thread A进入了synchronized区,然后thread B接着进入,那么,在thread B获取这个lock的时候,thread A的synchronized区和之前的代码的执行结果对于thread B来说,都是可见的,如图所示:


放在我们的例子中,假设Thread A先执行,先进入synchronized区域,那么当thread B后进入synchronzied区域,准备执行 b = 1 时,JVM保证thread A的
      synchronized (this) {x = b;}
这个代码执行效果对于thread B是可见的,不仅如此,连synchronzied区域之前的代码
      a = 1;
也必须是可见的,所以,如果是thread A先进入synchronized区域的话,那么只能是:

thread A:     synchronized{x = b;} ----> a = 1 ------------------------------------------------------
thread B:     -------------------------- -----------------------> synchronized{b = 1;}----> y = a ---

或者

thread A:     a = 1 ----> synchronized{x = b;} --------------------------------------------------------
thread B:     ----------------------------------------------------> synchronized{b = 1;}----> y = a ---

它们的结果都是 x = 0, y = 1。


有了这个结论,再回到本文最前面的我报bug的那个例子。可以知道那个NullPointException并不是由于所谓的“乱序执行”所导致的,应该是程序其它某个地方没有写好。所以,之前所谓的“Java Memory Model引发的血案”是不成立的,退堂。(对不住各位看官了....)

抱歉!评论已关闭.