JMM定義
JMM 即 Java Memory Model荧呐,也叫 Java 內(nèi)存模型。JMM 就是一種規(guī)范惑申,它定義了什么情況開發(fā)者不需要去感知計(jì)算機(jī)的各種重排序歧寺,什么情況需要開發(fā)者去干涉重排序,以保證程序的執(zhí)行結(jié)果可預(yù)測(cè)揣云。
JMM的由來(lái)
計(jì)算機(jī)這么多年來(lái)整體運(yùn)行速度不斷地提升捕儒,除了像CPU時(shí)鐘頻率、內(nèi)存讀寫速度等硬件性能不斷提升之外邓夕,還要?dú)w功于計(jì)算機(jī)科學(xué)家對(duì)于計(jì)算機(jī)對(duì)于各種指令處理效率的不斷優(yōu)化刘莹,包括超標(biāo)量流水線技術(shù),動(dòng)態(tài)指令調(diào)度焚刚,猜測(cè)執(zhí)行点弯,多級(jí)緩存技術(shù)等。在這其中矿咕,允許重排序?qū)τ谟?jì)算機(jī)運(yùn)行效率的提升產(chǎn)生了重要的作用抢肛,但同時(shí)也帶來(lái)了一些問題。計(jì)算機(jī)只能確保單線程情況下重排序?qū)τ谶\(yùn)行結(jié)果沒有影響碳柱,對(duì)于多線程就無(wú)能為力了捡絮。這個(gè)時(shí)候就需要一個(gè)規(guī)范來(lái)保證開發(fā)者既能享受重排序帶來(lái)的性能的提升又能讓復(fù)雜情況下的運(yùn)行結(jié)果可控,JMM 就是這樣一個(gè)規(guī)范莲镣。JMM 規(guī)定了 JVM 必須遵循的一組最小保證福稳,這組保證規(guī)定了對(duì)變量的操作何時(shí)對(duì)其他線程可見。換句話說(shuō)瑞侮,JMM 對(duì)內(nèi)存可見性作出了一些承諾的圆,在承諾之外鼓拧,開發(fā)者需要自己去處理內(nèi)存可見性問題。
內(nèi)存可見性問題
上面提到了內(nèi)存可見性問題略板,那么毁枯,什么是內(nèi)存可見性問題。
內(nèi)存可見性問題的核心是 CPU 的緩存與主內(nèi)存不一致叮称。
那么,這里就涉及到計(jì)算機(jī)原理的部分知識(shí)藐鹤,下圖是 X86 架構(gòu)下 CPU 緩存的布局:
從圖中可以看出 CPU 有多級(jí)緩存瓤檐,每個(gè)核心的一二級(jí)緩存數(shù)據(jù)都是該 CPU 核心私有的,由于有緩存一致性協(xié)議(例如 MESI )的存在娱节,各個(gè)核心的緩存之間不會(huì)存在不同步的問題挠蛉。
這里簡(jiǎn)單講一下緩存一致性協(xié)議 MESI,當(dāng)各個(gè) CPU 核心都緩存了一個(gè)共享變量時(shí)肄满,有任何一個(gè)核心對(duì)它作出了修改都會(huì)讓其他核心內(nèi)對(duì)應(yīng)變量的緩存單元失斍垂拧(這里失效的是整個(gè) CacheLine,不僅僅是變量所占用的區(qū)域)并且把修改值同步到主內(nèi)存稠歉。其他核心如果后續(xù)要操作這個(gè)變量掰担,必須從主內(nèi)存讀,這樣就可以保證各個(gè)緩存的一致性怒炸。
但引入緩存一致性協(xié)議會(huì)有很大的性能損耗带饱,為了解決這個(gè)問題,又進(jìn)行了各種優(yōu)化阅羹,這其中就有在計(jì)算單元和一級(jí)緩存之間引入 StoreBuffer 和 LoadBuffer 勺疼,如下圖所示:
StoreBuffer 和 LoadBuffer 的引入,大大提升了計(jì)算機(jī)性能捏鱼,但同時(shí)也帶來(lái)了一些問題:各級(jí)緩存之間數(shù)據(jù)是一致的执庐,但 StoreBuffer 和 LoadBuffer 一級(jí)緩存之間的數(shù)據(jù)卻是異步的,這里就會(huì)存在一致性問題导梆。
當(dāng)一個(gè)緩存中的數(shù)據(jù)被修改后轨淌,會(huì)存到 StoreBuffer 中,而 StoreBuffer 不會(huì)立即把修改后的數(shù)據(jù)同步到主內(nèi)存问潭,這時(shí)其他核心在主內(nèi)存中讀取到就是舊數(shù)據(jù)猿诸,也就是說(shuō)一個(gè)數(shù)據(jù)在一個(gè)核心的寫操作會(huì)出現(xiàn)對(duì)其他核心不可見的情況,這就是內(nèi)存可見性問題狡忙。
重排序
上面講的內(nèi)存可見性問題其本質(zhì)就是 CPU 內(nèi)存重排序梳虽,它是重排序的一種。這里講一下什么是重排序灾茁。
重排序分為三種:編譯重排序窜觉、CPU 指令重排序和 CPU 內(nèi)存重排序谷炸。
- 編譯器重排序:對(duì)于沒有先后依賴的語(yǔ)句,編譯器可以重新調(diào)整語(yǔ)句的順序禀挫;
- CPU 指令重排序:對(duì)于沒有先后依賴的指令并行執(zhí)行旬陡;
- CPU 內(nèi)存重排序:CPU 有自己的緩存,指令的執(zhí)行順序與寫入主內(nèi)存的順序不一定一致语婴。
編譯器重排序?qū)﹂_發(fā)者來(lái)說(shuō)是無(wú)感知的描孟,我們主要關(guān)注的是 CPU 指令重排序和 CPU 內(nèi)存重排序,這兩者都會(huì)對(duì)運(yùn)行結(jié)果產(chǎn)生影響砰左。
舉個(gè)例子:假如有 X匿醒,Y,a缠导,b 四個(gè)共享變量廉羔,我們?cè)趦蓚€(gè)不同的線程分別執(zhí)行下面的代碼:
線程一:
X = 1;
a = Y;
線程二:
Y = 1;
b = X;
這兩個(gè)線程的執(zhí)行順序是不一定的,有可能是順序執(zhí)行僻造,也可能是交叉執(zhí)行憋他,最終結(jié)果可能是:
- a = 0, b = 1 (線程一執(zhí)行 -> 線程二執(zhí)行)
- b = 0, a = 1 (線程二執(zhí)行 -> 線程一執(zhí)行)
- a = 1, b = 1 (兩個(gè)線程交叉執(zhí)行)
上面就是 CPU 指令重排序產(chǎn)生的影響。但實(shí)際情況會(huì)有第四種結(jié)果:
- a = 0, b = 0 (內(nèi)存重排序)
導(dǎo)致這個(gè)結(jié)果的原因是兩個(gè)線程全部或其中一個(gè)的寫入操作沒有同步到主內(nèi)存中髓削,因此給 a 或 b 賦值時(shí)讀取到的還是舊值 0竹挡,這就是內(nèi)存可見性問題。
CPU 指令重排序問題我們可以通過(guò)鎖蔬螟、CAS 等同步機(jī)制來(lái)解決此迅,編譯器重排序和 CPU 內(nèi)存重排序都可以通過(guò)引入內(nèi)存屏障來(lái)解決,這里主要關(guān)注內(nèi)存屏障在 CPU 重排序的應(yīng)用旧巾。
內(nèi)存屏障
內(nèi)存屏障是一個(gè)比較底層的概念耸序,它能對(duì)重排序作一定的限制,不同的內(nèi)存屏障對(duì)重排序限制不同鲁猩,一般都是組合使用的坎怪。作為 Java 開發(fā)者我們知道使用 volatile 關(guān)鍵字修飾的變量不會(huì)存在內(nèi)存可見性問題,它的原理其實(shí)就是在對(duì)變量的操作前后都加入了兩個(gè)不同的內(nèi)存屏障廓握,以保證所有的讀寫組合都不會(huì)發(fā)生內(nèi)存可見性問題搅窿。
可以把內(nèi)存屏障分為四類:
- LoadLoad:禁止讀和讀的重排序
- StoreStore:禁止寫和寫的重排序
- LoadStore:禁止讀和寫的重排序
- StoreLoad:禁止寫和讀的重排序
JDK 8 開始,Unsafe 類提供了三個(gè)內(nèi)存屏障方法:
public final class Unsafe {
// ...
public native void loadFence();
public native void storeFence();
public native void fullFence();
// ...
}
這三個(gè)方法對(duì)應(yīng)的內(nèi)存屏障如下:
- loadFence = LoadLoad + LoadStore
- storeFence = StoreStore + LoadStore
- fullFence = loadFence + storeFence + StoreLoad
我們平常在開發(fā)中一般不會(huì)去主動(dòng)使用內(nèi)存屏障隙券,而內(nèi)存屏障所實(shí)現(xiàn)的效果可以用 happen-before 來(lái)描述男应。
happen-before
首先來(lái)說(shuō)說(shuō)什么是 happen-before:它用來(lái)描述來(lái)個(gè)操作之間的內(nèi)存可見性,如果 A 操作 happen-before 于 B 操作娱仔,那么 A 操作的執(zhí)行結(jié)果必須是對(duì) B 操作可見的沐飘,這里隱含了一個(gè)條件,只有在 A 操作的執(zhí)行實(shí)際發(fā)生在 B 操作之前,這個(gè)可見性保證才會(huì)有效耐朴,happen-before 并不會(huì)去改變 A 和 B 的執(zhí)行順序借卧。
JMM 規(guī)范借助 happen-before 可以更好的描述出來(lái)。
happen-before 有以下四個(gè)基本規(guī)則:
- 單線程中的每個(gè)操作筛峭,happen-before于該線程中任意后續(xù)操作铐刘。
- 對(duì)volatile變量的寫,happen-before于后續(xù)對(duì)這個(gè)變量的讀影晓。
- 對(duì)synchronized的解鎖镰吵,happen-before于后續(xù)對(duì)這個(gè)鎖的加鎖。
- 對(duì)final變量的寫挂签,happen-before于final域?qū)ο蟮淖x捡遍,happen-before于后續(xù)對(duì)final變量的讀。
除了以上四個(gè)基礎(chǔ)規(guī)則之外竹握,happen-before 還具有傳遞性。傳遞性是指當(dāng) A happen-before 于 B辆飘,B happen-before 于 C 啦辐,那么操作 A 的結(jié)果一定對(duì)操作 C 可見。
這四個(gè)基本規(guī)則再加上 happen-before 的傳遞性蜈项,就構(gòu)成了 JMM 對(duì)開發(fā)者的整個(gè)承諾芹关。在這個(gè)承諾之后的部分,開發(fā)者就需要小心處理內(nèi)存可見性問題紧卒。