首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 理論與實踐:修復 Java 記憶體模型(上)

     編輯:Java 相關知識

前言

歷經近三年,JSR 133 近期發布修復 Java 記憶體模型的公開建議。原始 JMM 有幾個嚴重缺陷,導致某些本被認為簡單的概念語義(如 volatile、final、synchronized)難度驚人。本文展示如何加強 volatile 與 final 語義以修復 JMM,部分變更已集成 JDK 1.4,其餘將在 JDK 1.5 包含。

Java 平台與並發挑戰

Java 平台將執行緒與多處理技術整合進語言,程度遠超多數先前程式語言。語言對平台獨立並發與多執行緒的支援雄心勃勃且具開創性,或許不驚訝,問題比 Java 架構設計者原始構想稍難。關於同步與執行緒安全的許多底層混淆源於 Java 記憶體模型 (JMM) 難以直覺的細微差別,最初在 Java Language Specification 第 17 章指定,由 JSR 133 重新指定。

例如,非所有多處理器系統都表現出緩存一致性;若一處理器有更新的變量值在緩存中但未存入主記憶體,別處理器可能看不到更新值。緩存缺乏一致性時,兩不同處理器可看到同一記憶體位置有兩不同值。這聽起來不可能,但係故意 — 為獲取更高性能與擴展性 — 卻加重開發者與編譯器編寫程式碼解決此問題的負擔。

什麼是記憶體模型,為何需要?

記憶體模型描述程式中各變量(實例域、靜態域、陣列元素)間關係,以及在實際電腦系統中將變量存儲到記憶體和從記憶體取出的低層細節。物件最終存儲在記憶體,但編譯器、執行庫、處理器或緩存可有特權定時在變量指定記憶體位置存入或取出值。例如編譯器為優化迴圈索引變量,可能選擇存入暫存器,或緩存延遲至更適合時間才存入主記憶體。所有這些優化為實現更高性能,通常對用戶透明,但對多處理系統,這些複雜事務有時會完全顯現。

JMM 允許編譯器和緩存以數據在處理器特定緩存(或暫存器)和主記憶體間移動的次序擁有重要特權,除非程式設計師已使用 synchronized 或 final 明確請求某些可見性保證。這意味著無同步時,從不同執行緒角度看,記憶體操作以不同次序發生。

相對地,C 和 C++ 等語言無顯式記憶體模型 — 但 C 程式繼承執行程式處理器的記憶體模型(儘管給定架構編譯器可能知曉底層處理器記憶體模型某些情況,並承擔部分保持一致性責任)。這意味著並發 C 程式可在一處理器架構正確運行,卻在另一失效。雖 JMM 初始有些混亂,但有巨大好處 — 根據 JMM 正確同步的程式能正確運行在任何支援 Java 的平台上。

原始 JMM 的缺點

雖 JLS 第 17 章指定的 JMM 是雄心嘗試,定義一致的跨平台記憶體模型,但有細微而重要缺點。synchronized 和 volatile 語義令人困惑,致使許多有見地開發者有時選擇忽略規則,因在舊模型下編寫正確同步程式碼極難。

舊 JMM 允許奇怪混亂之事,如 final 欄位似乎無建構函式中設置的值(使想像中不可變物件並非不可變)和記憶體操作重排的意外結果。這也阻礙其他有效編譯器優化。若閱讀過雙重檢查鎖定問題文章,將記得記憶體操作重排多麼混亂,以及未正確同步(或未積極嘗試避免同步)時,細微卻嚴重問題如何潛伏在程式碼中。更糟者,許多未正確同步程式在某些情況下似乎工作良好,如輕負載、單處理器系統、或擁有比 JMM 要求更強記憶體模型的處理器上。

「重排序」術語

描述幾種對記憶體操作真實明顯重排序類型:

  • 編譯器不改變程式語義時,作為優化可隨意重排某些指令。
  • 某些情況下,允許處理器以顛倒次序執行操作。
  • 通常允許緩存以與程式寫入變量不同次序存入主記憶體。
  • 從另一執行緒角度看,任何這些條件引發操作以不同於程式指定次序發生 — 忽略重排序源碼時,記憶體模型視所有條件同等。

JSR 133 目標

JSR 133 授權修復 JMM,有幾目標:

  • 保留現有安全保證,含型別安全。
  • 提供無中生有安全性 — 變量值非「無中生有」創建,即執行緒觀察到變量值 X,必須有執行緒先真正寫入 X。
  • 「正確同步」程式語義盡可能簡單直觀。正式且直觀定義「正確同步」(兩定義一致)。
  • 程式設計師有信心創建多執行緒程式。無魔法使並發程式變易,但目標減輕程式設計師理解記憶體模型所有細節負擔。
  • 跨廣泛流行硬體架構上的高性能 JVM 實現可能。現代處理器記憶體模型差異大;JMM 應適應盡可能多實際架構,不犧牲性能。
  • 提供同步習慣用法,允許發布物件且無同步可見。此為初始化安全新安全保證。
  • 對現有程式碼只有最小影響。

值得注意的是,有漏洞技術(如雙重檢查鎖定)在新模型下仍有漏洞,「修復」雙重檢查鎖定非新模型目標(但 volatile 新語義允許通常提出的其中一個雙重檢查鎖定可選方法正確工作,儘管不鼓勵此技術)。

從 JSR 133 活躍三年來,發現這些問題比他們認為重要的任何問題都更微妙。這是開拓者代價!最終正式語義比原預料複雜得多,實際採用完全不同形式,但非正式語義清晰直觀,將在第二部分概要說明。

同步與可見性

大多數程式設計師知 synchronized 強制互斥鎖,防止多執行緒同時進入給定監視器保護的同步語句塊。但同步另一面:按 JMM 指定,它強制某些記憶體可見性規則。確保同步塊存在時緩存更新,進入同步塊時緩存失效。因此,在給定監視器保護的同步塊期間,一執行緒寫入的值對所有執行同一監視器保護同步塊的其餘執行緒可見。也確保編譯器不會將指令從同步塊內部移到外部(雖某些情況下會從外部移到內部)。JMM 無同步時不做此保證 — 這就是多執行緒存取同一變量必須用同步(或其同胞 volatile)的原因。

問題 1:不可變物件非不可變

JMM 最驚人缺點:不可變物件似乎可改變值(此類物件不變性旨由 final 關鍵字保證)。(提醒:讓物件所有欄位 final 不一定使物件不可變 — 所有欄位還必須是基本型別或不可變物件引用。)不可變物件(如 String)被認為不需同步。但因記憶體寫入變更從一執行緒傳播到另一執行緒存在潛在延遲,可能存在競態條件,允許一執行緒先見不可變物件一值,一段時間後見不同值。

如何發生?以 Sun 1.4 JDK String 實現為例,基本有三重要決定性欄位:字元陣列引用、長度、描述字串開始的字元陣列偏移量。String 此實現而非僅有字元陣列,故字元陣列可在多 String 和 StringBuffer 間共享,無需每次創建 String 將文本拷貝到新陣列。如 String.substring() 創建可與原 String 共享同一字元陣列的新字串,僅長度與偏移量不同。

假設執行:

String s1 = "/usr/tmp";
String s2 = s1.substring(4); // contains "/tmp"

字串 s2 長度 4、偏移量 4,但共享 s1 含 "/usr/tmp" 同一字元陣列。String 建構函式運行前,Object 建構函式用預設值初始化所有欄位,含決定性長度和偏移欄位。String 建構器運行時,字串長度和偏移量設為所需值。但舊模型下,無同步時,可能另一執行緒臨時見偏移欄位有預設值 0,後見正確值 4。結果 s2 值從 "/usr" 變 "/tmp"。非我們所要,且非所有 JVM 或平台可能,但舊模型規範允許。

問題 2:重排序 volatile 與非 volatile 存儲

另一主要領域與 volatile 欄位記憶體操作重排序有關,現有 JMM 引起非常混亂結果。現有 JMM 指出 volatile 讀寫直接與主記憶體打交道,避免存入暫存器或繞過處理器特定緩存。這使多執行緒一般能看見給定變量最新值。但結果此 volatile 定義並非最初想像那麼有用,導致 volatile 實際意義重大混亂。

為無同步提供較好性能,編譯器、執行庫、緩存通常允許重排普通記憶體操作,只要當前執行緒分辨不出區別(即執行緒內似乎是串行語義)。但 volatile 讀寫完全跨執行緒安排,編譯器或緩存不能在彼此間重排 volatile 讀寫。遺憾地,參考普通變量讀寫,JMM 允許 volatile 讀寫被重排,意味著不能用 volatile 標誌作操作完成指示。考慮下列程式碼,意圖假定 volatile 欄位 initialized 指示初始化完成。

清單 1:使用 volatile 欄位作「守衛」變量

Map configOptions;
char[] configText;
volatile boolean initialized = false;
. .
// In thread A
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText, configOptions);
initialized = true;
. .
// In thread B
while (!initialized)
  sleep();
// use configOptions

此處想法用 volatile 變量 initialized 擔任守衛表明一套別操作已完成。好想法,但舊 JMM 下不工作,因舊 JMM 允許非 volatile 寫(如寫入 configOptions 欄位、寫入 configOptions 引用 Map 的欄位)與 volatile 寫重排,故另一執行緒可能見 initialized 為 true,但對 configOptions 欄位或其引用物件尚無一致或當前視圖。volatile 舊語義只承諾正在讀寫變量可見性,不承諾其他變量。雖此法較易有效實現,但結果無原來想像那麼有用。

結語

按 JLS 第 17 章指定,JMM 有嚴重缺點,允許一些看似合理程式發生非直觀或不合需要之事。若正確編寫並發類太難,則可說許多並發類不能按預期工作,這是平台缺點。幸運的是,我們可在不破壞舊模型下正確同步任何程式碼同時,創建與多數開發者直覺更一致的記憶體模型,一切已由 JSR 133 完成。下月介紹新記憶體模型(大部分功能已集成 1.4 JDK)詳情。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved