好吧,我標題黨了。
之前的blog
簡單的提及過java Memory Model,不過這次卻真的碰到了由於JMM的特性導致的錯誤。
背景是跑系統的壓力測試的時候突然拋出了一個NullPointerException。這事挺奇怪的,因為已經跑了很長時間的壓力測試,可這會才莫名的報了個錯,於是找到對應的程式碼,如下所示:
Thread A
this.data = new Data();<br />synchronized (this) {</p><p>this.isDone = true;<br />this.notify();}<br />
Thread B
synchronized (this) {<br />if (this.isDone) {<br />} else {<br />this.wait();<br />}<br />}<br />this.data.do(); // throw NullPointerException<br />
上面兩部分代碼分別有不同的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 (this) {<br />this.data = new Data();<br />this.isDone = true;<br />this.notify();}<br />
由於採用了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的效果)
---------------------------------- 華麗麗的分割線 --------------------------------------------------
這個問題基本上有答案了,重新梳理一下吧。
還是舉例說明:
public class Test {<br />int x = 0, y = 0;<br />int a = 0, b = 0;<br />public void task1() {<br />a = 1;<br />x = b;<br />}<br />public void task2() {<br />b = 1;<br />y = a;<br />}<br />}<br />
假設對於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
關於這一點,就不多說了。
現在的問題是,如果改變一下代碼,變成:
public class Test {<br />int x = 0, y = 0;<br />int a = 0, b = 0;<br />public void task1() {<br />a = 1;<br />synchronized (this) {<br />x = b;<br />}<br />}<br />public void task2() {<br />synchronized (this) {<br />b = 1;<br />}<br />y = a;<br />}<br />}<br />
那麼,是否還有可能出現:
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引發的血案”是不成立的,退堂。(對不住各位看官了....)