JMM - Java 內存模型

JMM定義

JMM 即 Java Memory Model,也叫 Java 內存模型。JMM 就是一種規范,它定義了什么情況開發者不需要去感知計算機的各種重排序,什么情況需要開發者去干涉重排序,以保證程序的執行結果可預測。

JMM的由來

計算機這么多年來整體運行速度不斷地提升,除了像CPU時鐘頻率、內存讀寫速度等硬件性能不斷提升之外,還要歸功于計算機科學家對于計算機對于各種指令處理效率的不斷優化,包括超標量流水線技術,動態指令調度,猜測執行,多級緩存技術等。在這其中,允許重排序對于計算機運行效率的提升產生了重要的作用,但同時也帶來了一些問題。計算機只能確保單線程情況下重排序對于運行結果沒有影響,對于多線程就無能為力了。這個時候就需要一個規范來保證開發者既能享受重排序帶來的性能的提升又能讓復雜情況下的運行結果可控,JMM 就是這樣一個規范。JMM 規定了 JVM 必須遵循的一組最小保證,這組保證規定了對變量的操作何時對其他線程可見。換句話說,JMM 對內存可見性作出了一些承諾,在承諾之外,開發者需要自己去處理內存可見性問題。

內存可見性問題

上面提到了內存可見性問題,那么,什么是內存可見性問題。

內存可見性問題的核心是 CPU 的緩存與主內存不一致。

那么,這里就涉及到計算機原理的部分知識,下圖是 X86 架構下 CPU 緩存的布局:

CPU架構圖.png

從圖中可以看出 CPU 有多級緩存,每個核心的一二級緩存數據都是該 CPU 核心私有的,由于有緩存一致性協議(例如 MESI )的存在,各個核心的緩存之間不會存在不同步的問題。

這里簡單講一下緩存一致性協議 MESI,當各個 CPU 核心都緩存了一個共享變量時,有任何一個核心對它作出了修改都會讓其他核心內對應變量的緩存單元失敗(這里失效的是整個 CacheLine,不僅僅是變量所占用的區域)并且把修改值同步到主內存。其他核心如果后續要操作這個變量,必須從主內存讀,這樣就可以保證各個緩存的一致性。

但引入緩存一致性協議會有很大的性能損耗,為了解決這個問題,又進行了各種優化,這其中就有在計算單元和一級緩存之間引入 StoreBuffer 和 LoadBuffer ,如下圖所示:

加入各種Buffer的CPU架構圖

StoreBuffer 和 LoadBuffer 的引入,大大提升了計算機性能,但同時也帶來了一些問題:各級緩存之間數據是一致的,但 StoreBuffer 和 LoadBuffer 一級緩存之間的數據卻是異步的,這里就會存在一致性問題。

當一個緩存中的數據被修改后,會存到 StoreBuffer 中,而 StoreBuffer 不會立即把修改后的數據同步到主內存,這時其他核心在主內存中讀取到就是舊數據,也就是說一個數據在一個核心的寫操作會出現對其他核心不可見的情況,這就是內存可見性問題。

重排序

上面講的內存可見性問題其本質就是 CPU 內存重排序,它是重排序的一種。這里講一下什么是重排序。

重排序分為三種:編譯重排序、CPU 指令重排序和 CPU 內存重排序。

  • 編譯器重排序:對于沒有先后依賴的語句,編譯器可以重新調整語句的順序;
  • CPU 指令重排序:對于沒有先后依賴的指令并行執行;
  • CPU 內存重排序:CPU 有自己的緩存,指令的執行順序與寫入主內存的順序不一定一致。

編譯器重排序對開發者來說是無感知的,我們主要關注的是 CPU 指令重排序和 CPU 內存重排序,這兩者都會對運行結果產生影響。

舉個例子:假如有 X,Y,a,b 四個共享變量,我們在兩個不同的線程分別執行下面的代碼:

線程一:

X = 1;
a = Y;

線程二:

Y = 1;
b = X;

這兩個線程的執行順序是不一定的,有可能是順序執行,也可能是交叉執行,最終結果可能是:

  • a = 0, b = 1 (線程一執行 -> 線程二執行)
  • b = 0, a = 1 (線程二執行 -> 線程一執行)
  • a = 1, b = 1 (兩個線程交叉執行)

上面就是 CPU 指令重排序產生的影響。但實際情況會有第四種結果:

  • a = 0, b = 0 (內存重排序)

導致這個結果的原因是兩個線程全部或其中一個的寫入操作沒有同步到主內存中,因此給 a 或 b 賦值時讀取到的還是舊值 0,這就是內存可見性問題。

CPU 指令重排序問題我們可以通過鎖、CAS 等同步機制來解決,編譯器重排序和 CPU 內存重排序都可以通過引入內存屏障來解決,這里主要關注內存屏障在 CPU 重排序的應用。

內存屏障

內存屏障是一個比較底層的概念,它能對重排序作一定的限制,不同的內存屏障對重排序限制不同,一般都是組合使用的。作為 Java 開發者我們知道使用 volatile 關鍵字修飾的變量不會存在內存可見性問題,它的原理其實就是在對變量的操作前后都加入了兩個不同的內存屏障,以保證所有的讀寫組合都不會發生內存可見性問題。

可以把內存屏障分為四類:

  • LoadLoad:禁止讀和讀的重排序
  • StoreStore:禁止寫和寫的重排序
  • LoadStore:禁止讀和寫的重排序
  • StoreLoad:禁止寫和讀的重排序

JDK 8 開始,Unsafe 類提供了三個內存屏障方法:

public final class Unsafe { 
    // ...
    public native void loadFence(); 
    public native void storeFence(); 
    public native void fullFence(); 
    // ...
}

這三個方法對應的內存屏障如下:

  • loadFence = LoadLoad + LoadStore
  • storeFence = StoreStore + LoadStore
  • fullFence = loadFence + storeFence + StoreLoad

我們平常在開發中一般不會去主動使用內存屏障,而內存屏障所實現的效果可以用 happen-before 來描述。

happen-before

首先來說說什么是 happen-before:它用來描述來個操作之間的內存可見性,如果 A 操作 happen-before 于 B 操作,那么 A 操作的執行結果必須是對 B 操作可見的,這里隱含了一個條件,只有在 A 操作的執行實際發生在 B 操作之前,這個可見性保證才會有效,happen-before 并不會去改變 A 和 B 的執行順序。

JMM 規范借助 happen-before 可以更好的描述出來。

happen-before 有以下四個基本規則:

  • 單線程中的每個操作,happen-before于該線程中任意后續操作。
  • 對volatile變量的寫,happen-before于后續對這個變量的讀。
  • 對synchronized的解鎖,happen-before于后續對這個鎖的加鎖。
  • 對final變量的寫,happen-before于final域對象的讀,happen-before于后續對final變量的讀。

除了以上四個基礎規則之外,happen-before 還具有傳遞性。傳遞性是指當 A happen-before 于 B,B happen-before 于 C ,那么操作 A 的結果一定對操作 C 可見。

這四個基本規則再加上 happen-before 的傳遞性,就構成了 JMM 對開發者的整個承諾。在這個承諾之后的部分,開發者就需要小心處理內存可見性問題。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容