1. 程式人生 > >Java記憶體模型Cookbook(四)指南(Recipes)

Java記憶體模型Cookbook(四)指南(Recipes)

作者:Doug Lea 翻譯:丁一

  1. 前言
  2. 指令重排
  3. 記憶體屏障
  4. 多處理器
  5. 指南

單處理器(Uniprocessors)

如果能保證正在生成的程式碼只會執行在單個處理器上,那就可以跳過本節的其餘部分。因為單處理器保持著明顯的順序一致性,除非物件記憶體以某種方式與可非同步訪問的IO記憶體共享,否則永遠都不需要插入屏障指令。採用了特殊對映的java.nio buffers可能會出現這種情況,但也許只會影響內部的JVM支援程式碼,而不會影響Java程式碼。而且,可以想象,如果上下文切換時不要求充分的同步,那就需要使用一些特殊的屏障了。

插入屏障(Inserting Barriers)

當程式執行時碰到了不同型別的存取,就需要屏障指令。幾乎無法找到一個“最理想”位置,能將屏障執行總次數降到最小。編譯器不知道指定的load或store指令是先於還是後於需要一個屏障操作的另一個load或store指令;如,當volatile store後面是一個return時。最簡單保守的策略是為任一給定的load,store,lock或unlock生成程式碼時,都假設該型別的存取需要“最重量級”的屏障:

  1. 在每條volatile store指令之前插入一個StoreStore屏障。(在ia64平臺上,必須將該屏障及大多數屏障合併成相應的load或store指令。)
  2. 如果一個類包含final欄位,在該類每個構造器的全部store指令之後,return指令之前插入一個StoreStore屏障。
  3. 在每條volatile store指令之後插入一條StoreLoad屏障。注意,雖然也可以在每條volatile load指令之前插入一個StoreLoad屏障,但對於使用volatile的典型程式來說則會更慢,因為讀操作會大大超過寫操作。或者,如果可以的話,將volatile store實現成一條原子指令(例如x86平臺上的XCHG),就可以省略這個屏障操作。如果原子指令比StoreLoad屏障成本低,這種方式就更高效。
  4. 在每條volatile load指令之後插入LoadLoad和LoadStore屏障。在持有資料依賴順序的處理器上,如果下一條存取指令依賴於volatile load出來的值,就不需要插入屏障。特別是,在load一個volatile引用之後,如果後續指令是null檢查或load此引用所指物件中的某個欄位,此時就無需屏障。
  5. 在每條MonitorEnter指令之前或在每條MonitorExit指令之後插入一個ExitEnter屏障。(根據上面的討論,如果MonitorExit或MonitorEnter使用了相當於StoreLoad屏障的原子指令,ExitEnter可以是個空操作(no-op)。其餘步驟中,其它涉及Enter和Exit的屏障也是如此。)
  6. 在每條MonitorEnter指令之後插入EnterLoad和EnterStore屏障。
  7. 在每條MonitorExit指令之前插入StoreExit和LoadExit屏障。
  8. 如果在未內建支援間接load順序的處理器上,可在final欄位的每條load指令之前插入一個LoadLoad屏障。(此郵件列表linux資料依賴屏障的描述中討論了一些替代策略。)

這些屏障中的有一些通常會簡化成空操作。實際上,大部分都會簡化成空操作,只不過在不同的處理器和鎖模式下使用了不同的方式。最簡單的例子,在x86或sparc-TSO平臺上使用CAS實現鎖,僅相當於在volatile store後面放了一個StoreLoad屏障。

移除屏障(Removing Barriers)

上面的保守策略對有些程式來說也許還能接受。volatile的主要效能問題出在與store指令相關的StoreLoad屏障上。這些應當是相對罕見的 —— 將volatile主要用於避免併發程式裡讀操作中鎖的使用,僅當讀操作大大超過寫操作才會有問題。但是至少能在以下幾個方面改進這種策略:

  • 移除冗餘的屏障。可以根據前面章節的表格內容來消除屏障:
Original => Transformed
1st ops 2nd => 1st ops 2nd
LoadLoad [no loads] LoadLoad => [no loads] LoadLoad
LoadLoad [no loads] StoreLoad => [no loads] StoreLoad
StoreStore [no stores] StoreStore => [no stores] StoreStore
StoreStore [no stores] StoreLoad => [no stores] StoreLoad
StoreLoad [no loads] LoadLoad => StoreLoad [no loads]
StoreLoad [no stores] StoreStore => StoreLoad [no stores]
StoreLoad [no volatile loads] StoreLoad => [no volatile loads] StoreLoad

類似的屏障消除也可用於鎖的互動,但要依賴於鎖的實現方式。 使用迴圈,呼叫以及分支來實現這一切就留給讀者作為練習。:-)

  • 重排程式碼(在允許的範圍內)以更進一步移除LoadLoad和LoadStore屏障,這些屏障因處理器維持著資料依賴順序而不再需要。
  • 移動指令流中屏障的位置以提高排程(scheduling)效率,只要在該屏障被需要的時間內最終仍會在某處執行即可。
  • 移除那些沒有多執行緒依賴而不需要的屏障;例如,某個volatile變數被證實只會對單個執行緒可見。而且,如果能證明執行緒僅能對某些特定欄位執行store指令或僅能執行load指令,則可以移除這裡面使用的屏障。但是所有這些通常都需要作大量的分析。

雜記(Miscellany)

JSR-133也討論了在更為特殊的情況下可能需要屏障的其它幾個問題:

  • Thread.start()需要屏障來確保該已啟動的執行緒能看到在呼叫的時刻對呼叫者可見的所有store的內容。相反,Thread.join()需要屏障來確保呼叫者能看到正在終止的執行緒所store的內容。實現Thread.start()和Thread.join()時需要同步,這些屏障通常是通過這些同步來產生的。
  • static final初始化需要StoreStore屏障,遵守Java類載入和初始化規則的那些機制需要這些屏障。
  • 確保預設的0/null初始欄位值時通常需要屏障、同步和/或垃圾收集器裡的底層快取控制。
  • 在構造器之外或靜態初始化器之外神祕設定System.in, System.out和System.err的JVM私有例程需要特別注意,因為它們是JMM final欄位規則的遺留的例外情況。
  • 類似地,JVM內部反序列化設定final欄位的程式碼通常需要一個StoreStore屏障。
  • 終結方法可能需要屏障(垃圾收集器裡)來確保Object.finalize中的程式碼能看到某個物件不再被引用之前store到該物件所有欄位的值。這通常是通過同步來確保的,這些同步用於在reference佇列中新增和刪除reference。
  • 呼叫JNI例程以及從JNI例程中返回可能需要屏障,儘管這看起來是實現方面的一些問題。
  • 大多數處理器都設計有其它專用於IO和OS操作的同步指令。它們不會直接影響JMM的這些問題,但有可能與IO,類載入以及動態程式碼的生成緊密相關。

致謝(Acknowledgments)

感謝下列人員的更正與建議:Bill Pugh, Dave Dice, Jeremy Manson, Kourosh Gharachorloo, Tim Harris, Cliff Click, Allan Kielstra, Yue Yang, Hans Boehm, Kevin Normoyle, Juergen Kreileder, Alexander Terekhov, Tom Deneau, Clark Verbrugge, Peter Kessler, Peter Sewell和Richard Grisenthwaite。


丁 一

英文名ticmy,本站的翻譯主編,主要負責譯文的翻譯和校對工作,目前就職於阿里巴巴,對併發程式設計、JVM執行原理、正則等頗有興趣;個人部落格:http://www.ticmy.com/;同時,還是戶外攝影愛好者。