Android SMP 入門

Android 3.0 以上版本的平台版本經過最佳化調整,可提供支援 多處理器架構本文件說明的問題 在 C、C++ 和 Java 中,為對稱式多處理器系統編寫多執行緒程式碼時可能會發生問題 程式設計語言 (為了方便起見,以下稱為「Java」 。這只是 Android 應用程式開發人員的基本概念,而非完整內容 則討論此主題

簡介

SMP 是「Symmetric Multi-Processor」的縮寫。它說明瞭 兩個或多個相同的 CPU 核心會共用主要記憶體存取權結束時間 幾年前,所有 Android 裝置都已啟用 UP (Uni-Processor)。

大多數 (甚至非所有):Android 裝置總是搭載多種 CPU,但 過去只有一種是用來執行應用程式,有些則管理不同的裝置 硬體設備 (例如無線電)。CPU 可能有不同的架構 執行於這類程式的程式無法使用主記憶體與各個 Pod 通訊 其他。

現今販售的大多數 Android 裝置都是以 SMP 設計為基礎。 因此較複雜競爭狀況 ,在多執行緒程式中,單向處理器可能不會造成明顯問題。 但可能會在兩個以上的執行緒中經常執行失敗 在不同核心上同時執行 此外,在不同的應用程式中執行時,程式碼可能較不容易失敗 處理器架構,甚至是使用相同的 這個架構的簡短總覽在 x86 上經過徹底測試的程式碼可能會導致 ARM 上無法正常運作。 使用較新型編譯器重新編譯時,程式碼可能會開始失敗。

本文件的其餘部分將會說明原因,以及需要採取的行動 確認您的程式碼能正常運作

記憶體一致性模型:為何 SMP 有些許不同

這是複雜主題的快速、精簡概覽。有些區域會 完整內容,但都不該造成誤導或錯誤。如同你 說明,則這些詳細資料通常不重要。

如要進一步瞭解這個主題,請參閱文件結尾的「參考資料」。

記憶體一致性模型 (通常簡稱為「記憶體模型」) 描述程式語言或硬體架構針對記憶體存取作業所做的保證。例如: 如果您寫入值到位址 A,然後寫入值來位址 B, 可保證每個 CPU 核心都會看到寫入作業 順序。

大多數程式設計師都習慣循序漸進 一致性,這就像這樣(Adve & Gharachorloo)

  • 所有記憶體作業似乎一次執行一個
  • 單一執行緒中的所有作業似乎都按照所述順序執行 而非該處理方的程式

假設我們有非常簡單的編譯器或解譯器 不會出乎意料的是 ,以完整地載入及儲存指示 每個存取權各有一個指示。為了簡化說明,我們也假設每個執行緒都會在其專屬的處理器上執行。

若您查看一小段程式碼,並發現它確實從 從依序一致的 CPU 架構中 找出程式碼實作而成 讀取和寫入作業會按照預期順序進行這可能是因為 CPU 實際上會重新排序指令,並延遲讀取和寫入作業 無法在裝置上執行的程式碼告訴 CPU 正在執行什麼動作 而不是以簡單的方式執行指示(我們將忽略 記憶體對應裝置驅動程式 I/O)。

我們先從一小段程式碼著手 通常稱為「卷積測試」

以下舉個簡單的例子,程式碼會在兩個執行緒上執行:

執行緒 1 執行緒 2
A = 3
B = 5
reg0 = B
reg1 = A

在此和未來的所有例子中,記憶體位置是以 大寫字母 (A、B、C) 和 CPU 暫存器以「reg」開頭。所有回憶集錦 最初為零指示會從上至下執行。這裡是討論串 1 把值 3 儲存在位置 A,然後在位置 B 儲存值 5。執行緒 2 將位置 B 的值載入 reg0,然後載入 也就是 reg1(請注意,我們是以同一順序撰寫, 其他)。

假設執行緒 1 和執行緒 2 會在不同的 CPU 核心上執行。個人中心 當您思考 多執行緒的程式碼

依序一致性可保證兩個執行緒都完成後, 執行時,暫存器處於下列其中一種狀態:

暫存器 狀態
reg0=5,reg1=3 可行 (執行緒 1 先執行)
reg0=0,reg1=0 可行 (執行緒 2 先執行)
reg0=0,reg1=3 可能的 (並行執行)
reg0=5,reg1=0 從未開啟

為了讓我們看到 B=5 的情況,在儲存至 A 之前,讀取或寫入作業必須以錯誤順序執行。使用 因而不可能實現的

單一處理器 (包括 x86 和 ARM) 通常會依序一致。 執行緒會以交錯方式執行,因為作業系統核心會在執行緒之間切換。大部分的 SMP 系統,包括 x86 與 ARM 不具一致性舉例來說,硬體通常會在記憶體儲存時進行緩衝儲存,以免記憶體無法立即到達,並讓其他核心看到。

細節視情況而定。例如 x86,但依序 保持一致,但仍保證 reg0 = 5 且 reg1 = 0 仍不可能。 系統會緩衝商店,但商店訂單則會保留。 但 ARM 則不然。緩衝儲存的順序不會保留,且儲存可能不會同時傳送至所有其他核心。這些差異對組裝程式設計人員而言非常重要。 不過,如下方所示,C、C++ 或 Java 程式設計師 設計程式時應將這類架構差異隱藏起來。

到目前為止,我們從未想過 重新排序指示。實際上,編譯器也會重新排序指令, 改善效能在本範例中,編譯器之後可能會決定 執行緒 2 中的程式碼需要 reg1 的值才會需要 reg0,因而載入 reg1 優先。或者,一些之前的程式碼可能已載入 A,而編譯器 可能會決定重複使用該值,而不是再次載入 A。無論是哪種情況 導致 reg0 和 reg1 的載入作業可能會重新排序

重新排序不同記憶體位置的存取權 在硬體或編譯器中 因為這不會影響單一執行緒的執行作業,且 也能大幅改善效能我們將看到,只要稍加留意,就能避免這項問題影響多執行緒程式的結果。

由於編譯器也可以重新排序記憶體存取權,因此這個問題實際上 都不是第一次使用 SMP即使在使用單一處理器,編譯器也可以重新排序載入項目, 而 Thread1 和 reg1 的 重新排序指示不過,如果編譯器未重新重新排序, 從不觀察到這個問題在大多數 ARM SMP 上,即使沒有編譯器重新排序,也可能會在執行成功的次數非常多時,看到重新排序的情況。除非您打算組裝組件 通常 SMP 往往會讓人較容易看到 從此開始。

無數據流量程式設計

幸好,通常可以輕鬆避免為 就能取得這些詳細資料如果您遵循一些簡單明瞭的規則, 但必須清除前面所有部分部分。 但遺憾的是,如果您 不小心違反這些規則

現代的程式設計語言鼓勵所謂的「無種族資料」 程式設計風格只要您承諾不會引入「資料競爭」 並避免使用少量結構,告知編譯器 並用硬體保證可以產生依序一致的結果這並不 因此可避免重新排序記憶體存取權這意味著 你必須遵守這些規則,才能分辨記憶體存取行為 再次訂購。這就像是告訴你,只要保證不會參觀香腸工廠,香腸就是美味可口的食物。資料競爭會揭示記憶體的陰謀論 重新排序。

什麼是「資料競爭」?

當至少有兩個執行緒同時存取相同的一般資料,且至少其中一個執行緒修改該資料時,就會發生「資料競爭」。依據「一般」 資料」我們指的是非同步處理物件 適用於執行緒通訊互斥鎖、條件變數、Java 易變性 (C++ 原子物件) 不是一般資料,以及其存取方式 都允許參加競速事實上,這些函式的用途是防止 其他人員的資料競爭 如需儲存大量結構化物件 建議使用 Cloud Bigtable

為了判斷兩個執行緒是否同時存取相同 記憶體位置,我們可以忽略上述的記憶體重新排序討論,以及 會假設序列一致性下列計畫沒有資料競爭 如果 AB 是正常的布林變數, 最初 false:

執行緒 1 執行緒 2
if (A) B = true if (B) A = true

由於作業沒有重新排序,因此兩個條件都會評估為 false,且 都不會更新任何變數因此沒有資料競爭。另有 無需考慮如果從 A 載入,可能會發生什麼情況 並儲存至 B 的 執行緒 1 是重新排序的。編譯器不允許重新排序 Thread 1,但將其改寫為「B = true; if (!A) B = false」。答案是 比方說,在小鎮的陽光下製作香腸

資料競爭已正式定義在基本的內建類型上,例如整數和 參照或指標同時指派給int 很明顯是資料競爭但 標準程式庫和 編寫 Java Collections 程式庫,讓您也瞭解 例如資料庫層級的資料競爭他們承諾不會引入資料競爭 否則就必須同時存取同一個容器 它會進行更新在下列情況中更新一個執行緒中的 set<T> 同時在另一個執行緒中讀取這些內容 可讓程式庫 資料競爭,因此可視為非正式,視為「程式庫層級的資料競爭」。 相反地,系統會在讀取時更新一個執行緒中的一個 set<T> 都不會導致資料競爭 在此情況下,程式庫保證不會引入 (低階) 資料競爭。

通常同時存取資料結構中的不同欄位 不會導致資料競爭不過 這項規則:C 或 C++ 中,位元欄位的連續序列被視為 單一「記憶體位置」存取這類序列中的任何位元欄位 將視為存取所有資料,以判斷 存在的資料競爭這反映出一般硬體無法發揮作用 不必同時讀取及重寫相鄰位元,就能更新個別位元。 Java 程式設計師沒有類似的問題。

避免資料競爭

新型程式設計語言提供多種同步處理方式 避免資料競爭下列是最基本的工具:

鎖定或互斥鎖
Mutexes (C++11 std::mutexpthread_mutex_t),或 Java 中的 synchronized 區塊,可確保特定程式碼區塊不會與存取相同資料的其他程式碼區塊同時執行。我們將一般提及這些設施和其他類似設施 「鎖定」。在存取共用之前,持續取得特定的鎖定 資料結構並在事後發布,避免發生資料競爭 資料結構同時確保更新和存取作業可完整執行,即沒有 資料結構的其他更新可以在中央執行。那是理所當然 目前最常用來防止資料競爭的工具。Java 的使用 synchronized 區塊或 C++ lock_guardunique_lock 確保鎖定功能正確釋放的 例外狀況。
揮發性/原子變數
Java 提供支援並行存取的 volatile 欄位 並未導入資料競爭從 2011 年開始支援 C 和 C++ 語意類似的 atomic 變數和欄位。這些 通常比鎖定功能更難使用,因為它們只用來確保 個別存取單一變數的情況是不可分割的。(在 C++ 中,這通常 延伸至簡單的讀取-修改-寫入作業,例如遞增。Java 需要特殊方法呼叫才能執行此操作。)與鎖定不同,volatileatomic 變數無法 直接用來防止其他執行緒幹擾較長的程式碼序列。

請特別注意,volatile 有很大的差異 在 C++ 和 Java 中的意義在 C++ 中,volatile 不會妨礙資料運作 但較舊的程式碼通常用來解決 atomic 物件。我們不建議這麼做;英吋 C++ 時,為可並行的變數使用 atomic<T> 由多個執行緒存取。C++ volatile 適用於 裝置註冊等。

C/C++ atomic 變數或 Java volatile 變數 可用來防止其他變數發生資料競爭如果 flag 宣告為 atomic<bool>atomic_bool 型別 (C/C++) 或 volatile boolean (Java),且初始為 false,則下列程式碼片段不會發生資料競爭:

執行緒 1 執行緒 2
A = ...
  flag = true
while (!flag) {}
... = A

由於 Thread 2 會等候設定 flag,因此 執行緒 2 中的 A 必須在其後發生,且不能與 傳送到 Thread 1 中的 A。因此,系統不會建立 Aflag 的競賽不算是資料競爭。 因為易變性/原子存取並不是「一般記憶體存取」。

必須執行此實作,防止或隱藏記憶體重新排序 使程式碼能正常運作,讓程式碼達到預期的行為。 這通常會造成易變性/原子記憶體存取行為 比一般存取成本高出許多

雖然前面的例子是無數據資料,但鎖定 Java 中的 Object.wait() 或 C/C++ 中的條件變數,通常 但另一個不需要反覆執行迴圈的情況下 正在消耗電量

記憶體重新排序顯示時

不含競速資料的程式設計通常也不必明確處理 可能會遇到記憶體存取權重新排序的問題不過,在某些情況下,系統會顯示重新排序的內容:
  1. 如果您的程式因發生錯誤而造成資料競爭, 可能會顯而易見地顯示編譯器和硬體轉換 這可能會讓計畫變得出乎意料舉例來說,如果我們忘記在前述範例中宣告 flag 為易變的,執行緒 2 可能會看到未初始化的 A。或者,編譯器可能會判定標記無法 執行緒 2 的迴圈中可能會變更,並將程式轉換為
    執行緒 1 執行緒 2
    A = ...
      flag = true
    reg0 = 旗標; 時 (!reg0) {}
    ... = A
    偵錯時,雖然您可以看到 flag 的正確性。
  2. C++ 提供明確放鬆的功能 順序一致性原子作業可以使用明確的 memory_order_... 引數。同樣地, java.util.concurrent.atomic 套件提供更多限制 一組相似設施,特別是 lazySet()。且 Java 的 程式設計師偶爾會為了取得類似效果,而刻意利用資料競爭。 這些工具都能大幅改善效能 無法降低程式設計複雜度。這部分的內容只是 下文
  3. 部分 C 和 C++ 程式碼是以舊版樣式編寫,而非完全以舊版樣式編寫 與目前的語言標準一致,此版本volatile 使用變數,而不是 atomic 變數,以及記憶體排序 明確禁止插入所謂的圍欄 障礙。這需要明確說明存取權的理由 以便重新排序及理解硬體記憶體模型Linux 核心仍使用這類程式碼樣式。這不應 用於新的 Android 應用程式,本章節也沒有討論到。

練習

記憶體一致性問題的偵錯作業可能非常困難。如果缺少 鎖定、atomicvolatile 宣告的原因 您可能就無法 透過偵錯工具檢視記憶體傾印,瞭解原因。屆時 發出偵錯工具查詢,CPU 核心可能全都觀察到 以及記憶體和 CPU 暫存器的內容 這就是「不可能」的狀態

C 語言禁止的做法

以下列舉一些錯誤程式碼的範例,並提供簡單的修正方式。在開始之前,我們必須討論如何使用基本語言 而不是每個特徵的分數

C/C++ 和「volatile」

C 和 C++ volatile 宣告是非常特殊的用途工具。 這些變數可防止編譯器重新排序或移除「揮發性」 存取。這對於存取硬體裝置註冊的程式碼非常有幫助 對應至多個位置或 setjmp。但 C 和 C++ volatile,與 Java 不同 volatile 不適用於執行緒通訊。

在 C 和 C++ 中,存取 volatile 資料可能會存取不具變動性的資料來重新排序 不可分割性保證。因此,volatile 無法用於在彼此之間分享資料 移至可攜式程式碼中的執行緒,甚至是單處理器。C volatile 通常不會 進而防止硬體重新排序 多執行緒的 SMP 環境這就是 C11 和 C++11 支援的原因 atomic 物件。請改用這些。

許多舊版 C 和 C++ 程式碼仍會濫用 volatile 進行執行緒通訊。這通常適用於與您有條件相符的資料 中搭配明確圍欄使用,或用於處理 記憶體的順序則並不重要但無法保證可以正常運作 與未來的編譯器搭配使用

範例

在多數情況下,建議您以鎖定方式 (例如 pthread_mutex_t 或 C++11 std::mutex),而不是 但我們會採用後者 來說明模型的結構 應用在實際情境中

MyThing* gGlobalThing = NULL;  // Wrong!  See below.
void initGlobalThing()    // runs in Thread 1
{
    MyStruct* thing = malloc(sizeof(*thing));
    memset(thing, 0, sizeof(*thing));
    thing->x = 5;
    thing->y = 10;
    /* initialization complete, publish */
    gGlobalThing = thing;
}
void useGlobalThing()    // runs in Thread 2
{
    if (gGlobalThing != NULL) {
        int i = gGlobalThing->x;    // could be 5, 0, or uninitialized data
        ...
    }
}

概念是分配結構、初始化其欄位, 我們就會將其儲存在全域變數中來「發布」。屆時 其他執行緒都可以看到,但由於已經完全初始化,因此沒關係。 對吧?

問題是,儲存至 gGlobalThing 的動作可能會在欄位初始化之前觀察到,通常是因為編譯器或處理器將儲存至 gGlobalThingthing->x 的順序重新排序。從 thing->x 讀取的另一個執行緒 查看 5、0,甚至尚未初始化的資料。

這裡的核心問題是 gGlobalThing 上的資料競賽。 如果 Thread 1 在 Thread 2 時呼叫 initGlobalThing() 呼叫 useGlobalThing()gGlobalThing 可以 讀取某些特徵的資料

您可以將 gGlobalThing 宣告為原子,即可修正這個問題。在 C++11 中:

atomic<MyThing*> gGlobalThing(NULL);

這麼做可確保其他執行緒看得到這些寫入內容 按正確順序排列這也保證能夠避免 在其他情況下允許,但不太可能在真實世界中出現的模式 Android 硬體。例如,確保我們無法看到 僅有部分寫入的 gGlobalThing 指標。

Java 禁止執行的功能

我們尚未討論部分 Java 語言功能,在 讓我們先來快速瞭解

就技術層面而言,Java 不需要程式碼不可限制資料。而且,少數精心編寫的 Java 程式碼在資料競爭情況下也能正常運作。不過,編寫此類程式碼是非常多的 可能較為困難,而且我們稍後會簡單討論。更糟的是,指定此類代碼含義的專家認為規格已不再正確。(這個規格對於無資料限制) 程式碼)。

我們現在要以不含數據資料的模型,這個模型 Java 提供 基本上與 C 和 C++ 相同的保證。這裡提到的語言 一些明確放寬依序一致性的基元,特別是 lazySet()weakCompareAndSet() 通話 位置:java.util.concurrent.atomic。 和 C 和 C++ 一樣,我們暫時會忽略這些錯誤。

Java 的「Syncd」和「揮發性」關鍵字

「synchronized」關鍵字提供 Java 語言內建的鎖定機制。每個物件都有相關聯的「監控」 並用於互斥存取權如果兩個執行緒嘗試「同步處理」的 其中一個物件,其中一個就會等待另一個物件完成

如上所述,Java 的 volatile T 是 C++11 的 atomic<T> 的類比。同時存取 volatile 欄位允許使用,不會導致資料競爭。 忽略 lazySet() 等人。與資料競爭,Java VM 的工作是 請確保結果仍以順序一致

請特別注意,如果執行緒 1 寫入 volatile 欄位,且 執行緒 2 隨後從相同的欄位讀取資料,然後看到 值,執行緒 2 也保證會看到先前建立的所有寫入 執行緒 1。就記憶體效應而言 與監控版本類似 從易變性讀取資料就好比收音器

C++ 的 atomic 有顯著差異: 如果我們寫入 volatile int x; Java 中的 x++x = x + 1 相同;該資料來源 執行原子載入、遞增結果,然後執行不可拆分的 也就是經過處理且會導入模型的資料 接著再透過特徵儲存庫與他人分享與 C++ 不同的是,整體遞增並非不可拆分。 原子遞增作業是由 java.util.concurrent.atomic

範例

以下是單調計數器的簡單錯誤實作方式:(Java) 理論及做法:管理波動)

class Counter {
    private int mValue;
    public int get() {
        return mValue;
    }
    public void incr() {
        mValue++;
    }
}

假設 get()incr() 是從多個 但我們想確保每個執行緒都能在偵測到 系統會呼叫 get()。最棘手的問題是 mValue++ 其實是三項作業:

  1. reg = mValue
  2. reg = reg + 1
  3. mValue = reg

如果兩個執行緒同時在 incr() 中執行, 檔案更新可能會遺失若要讓遞增單位成為不可部分完成的 incr()「已同步處理」。

但仍然損壞,尤其是 SMP 上。還有資料競爭 可存取 mValue 中,get() 可同時透過 incr()。在 Java 規則下,get() 呼叫可以是 看起來與其他程式碼不同例如,假設我們讀取 資料列的計數器,可能會導致結果不一致 這是因為我們重新排序的 get() 呼叫,可能是由硬體或 編碼器-解碼器架構我們可以透過宣告 get() 來進行同步處理,以便修正問題。經過這項變更後,程式碼顯然正確無誤。

很遺憾,我們已推出鎖定爭用功能的可能性 導致效能不彰與其宣告 get() 為 因此,我們可以透過「揮發性」宣告 mValue。(注意: incr() 仍需使用 synchronize,因為 而 mValue++ 不是單一不可分割的運算。) 這也可以避免所有資料競爭,因此能保留依序一致性。 incr() 會比較慢,因為它會產生監控器進入/退出額外負擔,以及與易失性儲存區相關的額外負擔,但 get() 會比較快,因此即使沒有競爭,如果讀取次數遠大於寫入次數,這也是個好方法。(如要瞭解如何完全移除同步處理區塊,請參閱 AtomicInteger)。

以下是與前述 C 範例類似的另一個範例:

class MyGoodies {
    public int x, y;
}
class MyClass {
    static MyGoodies sGoodies;
    void initGoodies() {    // runs in thread 1
        MyGoodies goods = new MyGoodies();
        goods.x = 5;
        goods.y = 10;
        sGoodies = goods;
    }
    void useGoodies() {    // runs in thread 2
        if (sGoodies != null) {
            int i = sGoodies.x;    // could be 5 or 0
            ....
        }
    }
}

這與 C 程式碼有相同的問題,也就是 sGoodies 上有資料競爭。因此,指派作業 sGoodies = goods可能會在 goods中的欄位。如果您使用 volatile 關鍵字宣告 sGoodies,系統會恢復序列一致性,並正常運作。

請注意,只有 sGoodies 參照本身是易變的。但對其中欄位的存取權則不會。sGoodies volatile 以及記憶體順序都會妥善保留,而 無法並行存取陳述式 z = sGoodies.x 會執行 MyClass.sGoodies 的揮發性載入 隨後是 sGoodies.x 的不變性載入作業。能在當地 參照 MyGoodies localGoods = sGoodies,則後續的 z = localGoods.x 將不會執行任何易變載入。

在 Java 程式設計中,常見的慣用語為「重複檢查」 上鎖」:

class MyClass {
    private Helper helper = null;
    public Helper getHelper() {
        if (helper == null) {
            synchronized (this) {
                if (helper == null) {
                    helper = new Helper();
                }
            }
        }
        return helper;
    }
}

概念是希望使用 Helper 的單一例項 與 MyClass 例項相關聯的物件。我們只能建立 因此我們透過專屬的 getHelper() 建立並傳回該值 函式。為避免兩個執行緒建立執行個體的競爭狀況,我們需要 也會同步處理物件建立作業不過,我們不想為 因此只有在每次呼叫都會加入「同步處理」區塊 helper 目前為空值。

這會在 helper 欄位上發生資料競爭。可以 與其他執行緒中的 helper == null 並行設定。

如要瞭解這可能會失敗的原因,請考慮將同樣的程式碼稍微重寫,就好像是編譯為類似 C 的語言一樣 (我已新增幾個整數欄位來代表 Helper’s 建構函式活動):

if (helper == null) {
    synchronized() {
        if (helper == null) {
            newHelper = malloc(sizeof(Helper));
            newHelper->x = 5;
            newHelper->y = 10;
            helper = newHelper;
        }
    }
    return helper;
}

沒有辦法防止硬體或編譯器 例如透過以下字詞重新排序商店:helper x/y 個欄位。其他討論串可以 helper 非空值,但其欄位尚未設定且可供使用。 如需更多詳細資訊和更多故障模式,請參閱 鎖定是附錄中的聲明無效」連結,該等詳細資訊。 Josh Bloch 的 Effective Java、 第 2 版。

解決方法有兩種:

  1. 執行簡易操作,然後刪除外部檢查。這可確保我們不會在同步區塊外檢查 helper 的值。
  2. 宣告 helper 易變性,只要稍做變更 中的範例 J-3 將可在 Java 1.5 及以上版本上正常運作。建議您進行 一下,你有一分鐘相信「沒錯」是的。)

以下是 volatile 行為的另一個示意圖:

class MyClass {
    int data1, data2;
    volatile int vol1, vol2;
    void setValues() {    // runs in Thread 1
        data1 = 1;
        vol1 = 2;
        data2 = 3;
    }
    void useValues() {    // runs in Thread 2
        if (vol1 == 2) {
            int l1 = data1;    // okay
            int l2 = data2;    // wrong
        }
    }
}

查看 useValues(),如果 Thread 2 尚未觀察到 更新至 vol1,則無法得知data1或 已設定「data2」。一旦看到 vol1 的更新,就會知道可以安全存取 data1 並正確讀取,而不會引入資料競爭。不過 無法對「data2」做出任何假設,因為該商店 是在易變性存放區後執行

請注意,volatile 無法用於防止重新排序其他與彼此競爭的記憶體存取作業。無法保證 來產生機器記憶體圍欄指示可用來 資料競爭的情形,只有在另一個執行緒符合 或特定條件

進行方式

在 C/C++ 中,請優先使用 C++11 同步處理類別,例如 std::mutex。如果沒有,請使用 對應的 pthread 作業 其中包括適當的記憶體圍欄,提供正確 (依序一致) 除非另有說明) 和效率行為建議你善加利用 正確。舉例來說,請注意,條件變數等待行為可能會意外啟動 且不會被發出訊號,因此應顯示在迴圈中。

除非資料結構 就像計數器一樣簡單地實作鎖定和解鎖 pthread Mutex 都需要單一原子作業,且在沒有競爭的情況下,通常比單一快取遺漏的成本還要低,因此如果您將 Mutex 呼叫替換為原子作業,實際上不會節省太多。採用無鎖定設計,輕鬆處理重要資料結構 確保資料結構的高階作業 看起來像是整體,而不只是個別的原子片段。

如果您會使用原子運算,請使用 memory_order... 或「lazySet()」可能會提供效能資訊 但需要更深入的瞭解 大部分的現有程式碼 之後就可以發現錯誤請盡量避免這種情況。 如果您的用途不符合下一節所述的用途 請確保您是專家或已經諮詢過相關專家。

避免在 C/C++ 中使用 volatile 進行執行緒通訊。

在 Java 中,並行問題通常要用 並選用適當的公用程式類別 java.util.concurrent 套件。程式碼編寫正確 的 SMP 版本。

最安全的做法或許是讓物件保持不變。物品 來自 Java 的 String 和整數保留資料等類別, 物件,因此避免這些物件發生資料競爭的所有可能性。 書籍有效 Java、2nd Ed. 在「項目 15:盡量減少可變動性」中已有具體說明。註解位置: 特別是宣告 Java 欄位「final」的重要性(Bloch)

提醒您,即使物件無法變更,也別忘了與其他物件通訊 不含任何同步處理類型的執行緒就是資料競爭。這在 Java 中偶爾可接受 (請參閱下文),但需要格外小心,且可能會導致程式碼不穩定。如果不太重視效能,請加入 volatile 宣告。在 C++ 中,傳送指標 沒有適當同步的不可變物件參照 任何資料競爭都會造成錯誤 在這種情況下,很可能會自從 舉例來說,接收執行緒可能會看到尚未初始化的方法表格 指標值。

如果現有的程式庫類別和不可變動類別都不是 Java synchronized 陳述式或 C++ lock_guard / unique_lock 應用於保護 可存取多個執行緒可存取的任何欄位。如果互斥鎖 根據您的情況,宣告共用欄位 volatileatomic,但請務必小心 瞭解執行緒之間的互動。這些宣告 可避免常見的並行程式設計錯誤,不過這類錯誤也能助您一臂之力 避免最佳化編譯器和 SMP 的神祕失敗 出錯。

請盡量避免 「發布中」物件參照,例如讓其他 呼叫執行緒。在 C++ 中,這較不重要,或選擇繼續 「無資料競爭」以及 Java 提供的建議但這樣做一定是個好建議,而且 至關重要 在 Java 安全性模型重要且不受信任的其他環境中執行 程式碼可能會存取「外洩」物件參照。 如果您選擇忽略我們的警告,並且使用我們提供的一些技術,也同樣必須要注意 , 請參閱 (Java 中的安全建構技術) 詳細資料

進一步瞭解弱記憶體順序

C++11 以上版本提供明確機制,可放寬序列 為無資料範圍的計畫提供一致性保證。煽情露骨內容 memory_order_relaxedmemory_order_acquire (載入量) ) 和 memory_order_release(僅限儲存) atomic 的引數 每項作業提供的保證強度都低於預設的保證 隱含,memory_order_seq_cstmemory_order_acq_rel 提供了 memory_order_acquirememory_order_release 保證:不可部分完成的讀取-修改寫入 作業。memory_order_consume 尚未充分指定或實作,因此無法發揮作用,目前應予以忽略。

Java.util.concurrent.atomic 中的 lazySet 方法類似於 C++ memory_order_release 儲存庫。Java 一般變數有時會用來取代 存取 memory_order_relaxed 次,但實際上 甚至更弱與 C++ 不同,沒有實體的未排序機制 存取宣告為 volatile 的變數。

除非有迫切的效能需求,否則一般應避免使用這些方法。針對 ARM 等低順序機器架構,使用這些架構 基本上,每處理一次原子運算,通常會循環到數十個機器循環。 在 x86 中,成效僅侷限於商店,且可能低於 值得注意的是 也不太合乎常理,增加核心數後,效益可能會降低 因為記憶體系統變得較受限

弱排序原子完整的語意相當複雜。一般而言 您將全盤掌握語言規則 也不會進入這個頁面例如:

  • 編譯器或硬體可以將 memory_order_relaxed 存取作業移至 (但不能移出) 受鎖定取得和釋放所界定的關鍵區段。也就是說 memory_order_relaxed 間商店的顯示順序可能未正確顯示, 即使彼此偏離某個重要區塊也無妨
  • 一般的 Java 變數遭濫用為共用計數器時,可能會顯示 以減少單一執行緒 其他執行緒。但 C++ 不可分割的內容 memory_order_relaxed

因此, 這裡提供的是少數慣用的慣用語 與弱式原子序間有落差其中許多僅適用於 C++。

非競速賽

常見情況是變數具有不可分割的形式,因為變數偶爾 與寫入並行讀取,但並非所有存取都會發生這個問題。 例如變數 因為讀取是關鍵部分之外的資料 所以可能必須保持完整 並受到鎖定保護在此情況下,如果讀取作業健康狀態不良 受相同的鎖定功能保護 無法進行競爭,因為不能同時執行寫入作業。此時, 非競賽存取 (本例中為載入項目) 以使用 memory_order_relaxed,不會變更 C++ 程式碼的正確性。 鎖定實作已針對其他執行緒的存取權,強制執行所需的記憶體排序,而 memory_order_relaxed 則指定基本上不需要為原子存取權強制執行額外的排序限制。

這在 Java 中並沒有類比。

實驗結果不會仰賴正確性

如果我們只使用賽車量來產生提示,通常也沒有關係 不對載入強制執行任何記憶體順序如果值為 因此,我們也無法可靠地利用此結果來推斷 其他變數。因此沒關係 而且負載 伴隨 memory_order_relaxed 引數提供。

常見的 也就是使用 C++ compare_exchange 以不可分割的形式,將 x 替換成 f(x)。 用於計算「f(x)」的 x 初始載入 不需要是可靠的如果我們判斷錯誤,compare_exchange 就會失敗,我們會重試。你可以使用 x 的初始載入 memory_order_relaxed 引數;僅限記憶體排序 與實際的 compare_exchange 相關。

已部分修改但未讀取的資料

有時資料會由多個執行緒並行修改,但只有在平行運算完成後才會進行檢查。不錯 以不可分割的形式 (例如 在 C++ 中使用 fetch_add(),或 atomic_fetch_add_explicit() (在 C 中) 多個執行緒,但這些呼叫的結果 一律忽略。產生的值只會在結尾處讀取 。

在此情況下,您無法判斷是否存取這類資料 因此,C++ 程式碼可能會使用 memory_order_relaxed 引數。

簡單的事件計數器就是常見的例子。因為 所以很常見的情況,值得觀察:

  • 使用 memory_order_relaxed 可改善效能。 但不一定能解決最重要的效能問題:每次更新 需要專屬存取權,才能保存計數器的快取行。這個 會導致每次新的執行緒存取計數器時,快取失敗。 如果經常更新,並在執行緒之間替換,速度就會更快 避免每次更新共用計數器 例如使用執行緒本機計數器,並在結尾加上這些計數器
  • 這項技術可以與上一節結合使用: 同時讀取概略值和不可靠的值 且所有作業皆使用 memory_order_relaxed。 但請務必將產生的值視為完全不可靠。 因為計數似乎已經增加 1 次 表示其他執行緒可計數 直到達到該區間為止 每次都會增加遞增點可能有 與先前的程式碼重新排序就先前提到的類似情況而言,C++ 確保此類計數器的第二次載入不會傳回小於同一執行緒中先前載入的值。除非 就會發生這類錯誤)。
  • 經常會找到嘗試計算概略值的程式碼 透過執行個別不可分割 (或不) 的方式讀取和寫入計數器值,但是 不會以完整原子的形式呈現遞增單位常見引數是 「夠接近了」效能計數器等。 但這通常不是。 情況頻繁的更新 如果有大量資料,通常這些資料都會出現在 但損失。如果是四核心裝置,多半可能會遺失超過一半的步數。 (簡易練習:建構兩個執行緒情境,在這類情境中, 但最終計數器值為 1) 進行更新。

簡單的旗標通訊

memory_order_release 儲存庫 (或讀取 - 修改 - 寫入) 確保之後若之後載入 memory_order_acquire (或讀取 - 修改 - 寫入) 讀取寫入的值, 也會觀察在 memory_order_release 商店。相反地 memory_order_release 之前不會觀察到 追蹤 memory_order_acquire 載入次數的商店。 與 memory_order_relaxed 不同,此方法允許這類不可分割作業 用來在兩個執行緒之間傳達進度。

舉例來說,我們可以重寫再次勾選的鎖定範例 例如 YAML 檔案

class MyClass {
  private:
    atomic<Helper*> helper {nullptr};
    mutex mtx;
  public:
    Helper* getHelper() {
      Helper* myHelper = helper.load(memory_order_acquire);
      if (myHelper == nullptr) {
        lock_guard<mutex> lg(mtx);
        myHelper = helper.load(memory_order_relaxed);
        if (myHelper == nullptr) {
          myHelper = new Helper();
          helper.store(myHelper, memory_order_release);
        }
      }
      return myHelper;
    }
};

取得負載和發布儲存庫,確保在發現非空值的情況下 helper,那麼我們也會看到其中的欄位已正確初始化。 我們也整合了之前的觀察 可以使用 memory_order_relaxed

Java 程式設計師可預期將 helper 表示為 java.util.concurrent.atomic.AtomicReference<Helper> 並使用 lazySet() 做為發布儲存庫。載入作業會繼續使用一般 get() 呼叫。

不論是哪一種情況,效能都只集中在初始化時 但這不太可能對效能至關重要 較易理解的入侵方式可能為:

    Helper* getHelper() {
      Helper* myHelper = helper.load(memory_order_acquire);
      if (myHelper != nullptr) {
        return myHelper;
      }
      lock_guard&ltmutex> lg(mtx);
      if (helper == nullptr) {
        helper = new Helper();
      }
      return helper;
    }

這種方式和快速路徑相同,但也可預設 依序一致,針對非效能影響慢速的作業 路徑。

即便在這裡,helper.load(memory_order_acquire) 可能會產生相同的程式碼 進行完全一致 (依序同步) 的架構 helper。這裡 絕對是最有益的最佳化 可能會導入myHelper來消除 第二次載入,不過之後的編譯器可能會自動執行。

取得/發布商品無法防止商店顯示 且無法確保其他執行緒看得到該商店 並以一致的順序排列因此,它不支援 Dekker 的互斥演算法所示的一種棘手但相當常見的程式碼模式:所有執行緒一開始都會設定標記,表示它們想要執行某項操作;如果執行緒 t 發現沒有其他執行緒嘗試執行某項操作,就可以安全地繼續執行,因為它知道不會受到干擾。將沒有其他討論串 t 的旗標,因此還是能繼續操作。操作失敗 如果該標記是透過取得/發行版本取得,由於沒有 避免在有人存取執行緒的旗標後延遲顯示 錯誤繼續。預設的 memory_order_seq_cst 確實會防止這類情況。

不可變動的欄位

如果物件欄位在首次使用時初始化,且從未變更,則可能會使用弱排序存取權限進行初始化,然後讀取該欄位。在 C++ 中,可以將其宣告為 atomic,並使用 memory_order_relaxed 存取;在 Java 中,可以不宣告 volatile,並在沒有特殊措施的情況下存取。這需要以下所有訴訟保留:

  • 應能從欄位本身的值辨別 不論狀態是否已初始化如要存取欄位 快速路徑測試與傳回值應該只讀取該欄位一次。 在 Java 中,後者是必要的。即使欄位測試是初始化 第二次載入時,可能會讀取之前的未初始化值。在 C++ 中 「朗讀一次」就是最理想的做法
  • 初始化和後續載入都必須是不可分割的 其中的部分更新不應顯示如果是 Java,則 而不應是 longdouble。若是 C++ 必須提供不可分割指派;建構模型並不可行 atomic 的結構並非不可部分完成的。
  • 由於多個執行緒,重複的初始化作業必須安全無虞 可能會同時讀取未初始化的值。在 C++ 中,這通常 後接「可複製」要求所有 原子型別;含有巢狀自有指標的型別 宣告 無法複製Java 使用者 可接受的參考檔案類型如下:
  • Java 參照僅限於只包含最終欄位的不可變動類型。不可變動類型的建構函式不應發布 物件參照。此範例使用 Java 最終欄位規則 請確定讀者看過參考資料後,也能看到 已初始化的最終欄位。C++ 沒有關於這些規則的類比和 基於這個原因 (在 除了違反「可複製」需求)。

關閉記事

雖然這份文件不只是淺談,但也沒有深入探討。這是一個非常廣泛且深入的話題。只有部分通知 深入探索:

  • 實際的 Java 和 C++ 記憶體模型會以 happens-before 關係,指定保證何時可執行兩項動作 按照特定順序發生我們定義資料競爭時,會以非正式的方式 談到「同時」處理 2 種記憶體存取行為。 說真的,不然是指彼此之前的事。 建議您瞭解 Java 或 C++ 記憶體模型中「發生在前」happens-before和「同步處理」synchronizes-with的實際定義。雖然直覺式的概念是「同時」通常良好 這些定義夠清楚易懂 嘗試在 C++ 中使用順序不足的原子運算。 (目前的 Java 規格僅定義 lazySet() 非正式地)。
  • 瞭解重新排序程式碼時,系統會允許和禁止哪些編譯器。 (JSR-133 規格提供了一些合法轉換的絕佳範例,這些轉換會導致意外的結果)。
  • 瞭解如何在 Java 和 C++ 中編寫不可變動的類別。這不僅僅是「在建構後不要變更任何內容」。
  • Effective (有效) 的「Concurrency」(並行) 部分的建議項目內部化 Java、第 2 版。舉例來說,您應避免使用 需在同步區塊中覆寫)
  • 請詳閱 java.util.concurrentjava.util.concurrent.atomic API,瞭解可用的功能。建議做法 並行註解,例如 @ThreadSafe@GuardedBy (來自 net.jcip.annotations)。

附錄中的「參考資料」一節提供文件和網站連結,可進一步說明這些主題。

附錄

實作同步處理儲存庫

(大部分的程式設計師都需要自行實作, 但討論品質非常有用。)

適用於小型內建類型 (例如 int),以及 Android、一般負載和商店指示,確保使用者 分享你的內容,你的可看到全部或完全無法看到 處理器載入相同位置這提供一些基本觀感 「原子性」完全免費。

如先前所述,這點並不足夠。為了確保序列式體驗 保持一致性,也需避免重新排序作業 讓其他程序可在 順序。事實上,Android 版應用程式會自動使用後者 硬體降幅時,我們得審慎選擇強制執行 所以我們通常會在這裡忽略它

同時防止重新排序,藉此保留記憶體作業順序 並防止硬體重新排序我們在這裡 文字

ARMv7、x86 和 MIPS 上的記憶體排序均採用 「圍欄」指示 盡量避免顯示圍欄後的指令 。這些常見用途 稱為「障礙物」但可能會造成混淆 pthread_barrier 式的障礙物,帶來許多好處 其他事項)使用者如要理解 圍欄指示是一個相當複雜的主題,必須處理 如何透過各種不同的圍欄提供保證 它們如何與其他訂單保證相結合 硬體元件這是概略的總覽 細膩刻劃這些細節

最基本的排序保證是由 C++ 提供 「memory_order_acquire」和「memory_order_release」 不可部分完成的作業:發布存放區之前的記憶體作業 應該在獲取負載之後顯示。在 ARMv7 上,這項規定由下列項目強制執行:

  • 請在商店指示前加上適當的圍欄指示。 這可避免所有先前的記憶體存取作業因儲存指令而重新排序。這也能避免 (稍後商店指示)。
  • 根據負載指示提供適合的圍欄指示, 防止載入被後續存取重新排序。 (同樣地,至少提前載入,提供不必要的排序)。

綜合這些準備,以用於 C++ 取得/發布版本。 對 Java volatile 而言,這些是必要元件但不夠充分 或 C++ 依序一致的 atomic

如要瞭解我們所需的其他部分,可以考慮 Dekker 演算法的片段 這在稍早提過 flag1flag2 是 C++ atomic 或 Java volatile 變數,最初都是 false。

執行緒 1 執行緒 2
flag1 = true
if (flag2 == false)
    critical-stuff
flag2 = true
if (flag1 == false)
    critical-stuff

依序一致性表示 flagn 必須優先執行,並由 另一個執行緒進行測試因此 這些執行緒同時執行「關鍵內容」。

不過,在取得「取得與購買來源」訂閱產品時,所需要的圍欄只會增加了 每個執行緒的開頭和結尾加入柵欄,這沒有幫助 此處。我們也必須確保 volatile/atomic 商店後方有 volatile/atomic 載入時,兩者並未重新排序。 一般而言,系統會在 因此也能一致地儲存 (這再次遠比要求高,因為這個圍欄通常是訂單 所有後續的記憶體存取權)

我們可以改為將額外的圍欄 一致性。由於儲存庫的使用頻率較低,因此我們所述的慣用做法更常見,也更常用於 Android。

如先前章節所述,我們需要插入商店/貨運障礙 作業。在 VM 中執行的揮發性存取程式碼會如下所示:

揮發性負載 揮發性商店
reg = A
fence for "acquire" (1)
fence for "release" (2)
A = reg
fence for later atomic load (3)

真正的機器架構通常可以提供 柵欄,用來排序不同類型的存取行為, 成本不盡相同選用外型巧妙且影響深遠 以便確保機構中的其他核心使用者可以 產生一致的記憶體順序,以及由 就能正確組成多個圍欄組合。詳情 請參閱劍橋大學網頁,網址為: 收集原子與實際處理器的對應關係

在某些架構中,特別是 x86 的「取得」和「release」 因此不需要跨越障礙,因為硬體一律採用隱含模式 才能確保排序充足因此,在 x86 中只有最後一個圍欄 (3) 生成式 AI 模型與 x86 類似,不可部分完成的讀取-修改-寫入 就是所謂的強大圍欄因此從未如此 需要任何圍欄在 ARMv7 上,上述所有柵欄都是必要的。

ARMv8 提供 LDAR 和 STLR 指令,可直接強制執行 Java 易變或 C++ 序列一致的載入和儲存要求。這可避免我們前面提到的非必要的重新排序限制。ARM 上的 64 位元 Android 程式碼會使用這些;我們選擇著重於 ARMv7 邊界放置位置,因為這可以讓您更瞭解實際需求。

其他資訊

深度或廣度的網頁和文件。一般來說, 文章會靠近清單頂端。

共用記憶體一致性模型:教學課程
在 1995 年由 Adve &如要進一步瞭解記憶體一致性模型,不妨從這裡著手。
http://www.hpl.hp.com/techreports/Compaq-DEC/WRL-95-7.pdf
記憶體阻礙
簡單說明問題。
https://zh.wikipedia.org/zh-tw/Memory_barrier
討論串基本概念
Hans Boehm 製作的 C++ 和 Java 多執行緒程式設計簡介。討論資料競爭和基本同步處理方法。
http://www.hboehm.info/c++mm/threadsintro.html
Java 並行實務
這本書於 2006 年發布,涵蓋了各種主題的詳盡說明。強烈建議任何人在 Java 中編寫多執行緒程式碼。
http://www.javaconcurrencyinpractice.com
JSR-133 (Java 記憶體模型) 常見問題
簡介 Java 記憶體模型,包括同步處理、易變變數和最終欄位建構的說明。(有稍微過時,在討論其他語言時更是如此)。
http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html
Java 記憶體模型中的程式轉換效力
對 Java 記憶體模型的其他問題進行相當技術性的說明。這些問題不適用於「無數據流量」 計畫。
http://citseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.112.1790&rep=rep1&type=pdf
java.util.concurrent 套件總覽
java.util.concurrent 套件的說明文件。頁面底部附近的「記憶體一致性屬性」部分,說明瞭各種類別所做的保證。
java.util.concurrent」套件摘要
Java 理論與實務:Java 中的安全建構技巧
本文將詳細說明在建構物件時逸出的參照機制,並提供適用於執行緒安全建構函式的指南。
http://www.ibm.com/developerworks/java/library/j-jtp0618.html
Java 理論與實務:管理波動
這篇實用文章會介紹 Java 中對變化性欄位的影響與無法達成的目標。
http://www.ibm.com/developerworks/java/library/j-jtp06197.html
「Double-Checked Locking is Broken」宣告
Bill Pugh 詳細說明在沒有 volatileatomic 的情況下,雙重檢查鎖定機制會遭到破壞的各種方式。包括 C/C++ 和 Java。
http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
[ARM] Barrier Litmus 測試和食譜
ARM SMP 問題的討論,以簡短的 ARM 程式碼摘要說明。如果您發現本頁的範例太過籠統,或想閱讀 DMB 指令的正式說明,請參閱這篇文章。此外,也說明瞭用於可執行程式碼的記憶體屏障 (如果您即時產生程式碼,這些操作說明可能很實用)。請注意,這會是 ARMv8 之前的版本, 支援額外的記憶體排序指示,並且使用更強 記憶體模型(詳情請參閱「ARM® 架構參考手冊 ARMv8,適用於 ARMv8-A 架構設定檔」)。
http://infocenter.arm.com/help/topic/com.arm.doc.genc007826/Barrier_Litmus_Tests_and_Cookbook_A08.pdf
Linux 核心記憶體屏障
Linux kernel 記憶體障礙的說明文件包含一些實用範例和 ASCII 圖片。
http://www.kernel.org/doc/Documentation/memory-barriers.txt
ISO/IEC JTC1 SC22 WG21 (C++ 標準) 14882 (C++ 程式設計語言) 第 1.10 節和第 29 節 (「原子運算程式庫」)
草擬 C++ 原子作業特徵的標準。這個版本為 包括這個領域中的小幅變更 從 C++11 開始
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4527.pdf
(簡介:http://www.hpl.hp.com/techreports/2008/HPL-2008-56.pdf)
ISO/IEC JTC1 SC22 WG14 (C 標準) 9899 (C 程式設計語言) 章節 7.16 (“Atomics <stdatomic.h>”)
草擬 ISO/IEC 9899-201x C 原子作業功能的標準。 如要瞭解詳情,請一併參閱後續的瑕疵報告。
http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf
C/C++11 與處理器對應 (劍橋大學)
Jaroslav Sevcik 和 Peter Sewell 的翻譯集合 C++ 原子組與各種常見處理器指示集。
http://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html
Dekker 的演算法
「並行程式設計共同排除問題的第一個已知正確解決方案」。維基百科文章擁有完整的演算法,並探討如何更新它以與新型最佳化編譯器和 SMP 硬體搭配使用。
https://zh.wikipedia.org/zh-tw/Dekkers_algorithm
比較 ARM 與 Alpha 版的註解,並處理依附元件
Catalin Marinas 在 arm-kernel 郵寄清單上發送的電子郵件。包含地址與控制項依附元件的良好摘要。
http://linux.derkeiler.com/Mailing-Lists/Kernel/2009-05/msg11811.html
記憶體每位程式設計師應該瞭解的事情
Ulrich Drepper 撰寫的文章,內容非常詳細,討論不同類型的記憶體,特別是 CPU 快取。
http://www.akkadia.org/drepper/cpumemory.pdf
導致 ARM 記憶體模型不一致的原因
本白皮書是由 Chong 和ARM, Ltd, 的 Ishtiaq. 嘗試以容易取得又便利的方式描述 ARM SMP 記憶體模型。本文所用的「可觀察性」定義來自這篇論文。再次提醒,這個舊版 ARMv8。
http://portal.acm.org/ft_gateway.cfm?id=1353528&type=pdf&collNOTdl 的完整 CFID=96099715&CFTOKEN=57505711
適用於編譯器作家的 JSR-133 教戰手冊
Doug Lea 編寫了 JSR-133 (Java 記憶體模型) 說明文件的隨附內容。內含初步的實作指南 這個 API 最適合用於編寫程式編寫者使用的 Java 記憶體模型 仍舊獲得熱烈迴響 甚至可能提供深入分析 但這裡討論的四種圍欄多樣性並非理想 比對 Android 支援的架構和上述 C++11 對應 現在對 Java 而言,現在是更加精確的食譜來源。
http://g.oswego.edu/dl/jmm/cookbook.html
x86-TSO:適合 x86 多處理器的嚴謹且實用的程式設計師模型
x86 記憶體模型的詳細說明。精確描述 不過,ARM 記憶體模型的複雜度特別高
http://www.cl.cam.ac.uk/~pes20/weakmemory/cacm.pdf