好吧,我标题党了。
之前的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引发的血案”是不成立的,退堂。(对不住各位看官了....)