讀古今文學網 > Java程序員修煉之道 > 4.6 Java內存模型 >

4.6 Java內存模型

Java語言規範(JLS)在第17.4節中介紹了JMM。其中的描述非常正式,用同步動作和被稱為偏序1的數學結構描述JMM。這對於語言理論學家和Java規範的實現者(編譯器和虛擬機的製造者)來說非常棒,但對於需要理解多線程代碼如何執行的應用開發人員來說,這種描述會讓他們頭昏腦脹。

1 設A是一個非空集,P是A上的一個關係,若關係P是自反的(對任意的a∈A,(a,a)∈P)、反對稱的(若(a,b)∈P且(b,a)∈P,則a=b)和傳遞的(若(a,b)∈P,(b,c)∈P,則(a,c)∈P),則稱P是集合A上的偏序關係。比如實數集上的小於等於關係(a<=a;a<=b,b<=a,則a=b;a<=b,b<=c,則a<=c)。——譯者注

我們在這裡不重複規範裡的正式描述,而是用兩個基本概念列出最重要的規則,這兩個概念是代碼塊之間的之前發生(Happens-Before)和同步約束(Synchronizes-With)關係。

  • 之前發生——這種關係表明一段代碼塊在其他代碼開始之前就已經全部完成了。
  • 同步約束——這意味著動作繼續執行之前必須把它的對象視圖與主內存進行同步。

如果你認真研究過OO編程,應該聽到過面向對像構件的Has-A和Is-A這兩種表述方式。一些開發人員發現,用之前發生和同步約束來描述基本的概念構件對理解Java並發很有幫助。這和Has-A與Is-A的道理一樣,但這兩組概念在技術上沒有直接關聯。

圖4-11中是一個易失性寫入與後續的讀取訪問(用於println)之間同步約束的例子。

圖4-11 同步約束的例子

JMM的主要規則如下。

  • 在監測對像上的解鎖操作與後續的鎖操作之間存在同步約束關係。
  • 對易失性(volatile)變量的寫入與後續對該變量的讀取之間存在同步約束關係。
  • 如果動作A受到動作B的同步約束,則A在B 之前發生。
  • 如果在程序中的線程內A出現在B之前,則A在B 之前發生。

前兩個規則通俗來說就是「先放後取」。換句話說,一個線程在寫入時持有的鎖要在其他操作(包括讀取)能夠獲取鎖之前被釋放掉。

這裡還有些規則,實際上是關於敏感行為的。

  • 構造方法要在那個對象的終結器開始運行之前完成(一個對像被終結之前必須已經構造完整)。
  • 開始一個線程的動作受到這個新線程的第一個動作的同步制約。
  • Thread.join受到被合併的線程的最後一個(和其他全部)動作的同步制約。
  • 如果X在Y 之前發生,並且Y在Z 之前發生,則X在Z 之前發生(傳遞性)。

這些簡單的規則定義了內存和同步如何工作的全平台視圖。圖4-12展示了傳遞性規則。

圖4-12 之前發生的傳遞性

注意 實際上,這些規則是JMM做出的最低保證。真正的JVM實際上可能表現得更好。對於開發人員來說,這可能是個陷阱,因為某個特定JVM中的行為實際上是個隱藏在底層並發中的詭異bug,卻很容易給人造成錯覺,以為是它提供的安全特性。

從這些最低保證中,很容易可以看出不可變性成為Java並發編程中的一個重要概念的原因。如果對像不可改變,確保改變對所有線程可見的相關問題就不會出現。