Java 內(nèi)存模型 JMM 淺析

JMM簡介

Java Memory Model簡稱JMM, 是一系列的Java虛擬機平臺對開發(fā)者提供的多線程環(huán)境下的內(nèi)存可見性瘾杭、是否可以重排序等問題的無關(guān)具體平臺的統(tǒng)一的保證丽已。(可能在術(shù)語上與Java運行時內(nèi)存分布有歧義护盈,后者指堆、方法區(qū)问裕、線程棧等內(nèi)存區(qū)域)织堂。

并發(fā)編程有多種風(fēng)格,除了CSP(通信順序進程)鸠删、Actor等模型外,大家最熟悉的應(yīng)該是基于線程和鎖的共享內(nèi)存模型了贼陶。在多線程編程中刃泡,需要注意三類并發(fā)問題:

1、原子性

2碉怔、可見性

3烘贴、重排序

原子性涉及到,一個線程執(zhí)行一個復(fù)合操作的時候撮胧,其他線程是否能夠看到中間的狀態(tài)桨踪、或進行干擾。典型的就是i++的問題了趴樱,兩個線程同時對共享的堆內(nèi)存執(zhí)行++操作馒闷,而++操作在JVM酪捡、運行時叁征、CPU中的實現(xiàn)都可能是一個復(fù)合操作, 例如在JVM指令的角度來看是將i的值從堆內(nèi)存讀到操作數(shù)棧、加上一逛薇、再寫回到堆內(nèi)存的i捺疼,這幾個操作的期間,如果沒有正確的同步永罚,其他線程也可以同時執(zhí)行啤呼,可能導(dǎo)致數(shù)據(jù)丟失等問題。常見的原子性問題又叫競太條件呢袱,是基于一個可能失效的結(jié)果進行判斷官扣,如讀取-修改-寫入。?可見性和重排序問題都源于系統(tǒng)的優(yōu)化羞福。

由于CPU的執(zhí)行速度和內(nèi)存的存取速度嚴(yán)重不匹配惕蹄,為了優(yōu)化性能,基于時間局部性治专、空間局部性等局部性原理卖陵,CPU在和內(nèi)存間增加了多層高速緩存,當(dāng)需要取數(shù)據(jù)時张峰,CPU會先到高速緩存中查找對應(yīng)的緩存是否存在泪蔫,存在則直接返回,如果不存在則到內(nèi)存中取出并保存在高速緩存中〈現(xiàn)在多核處理器越基本已經(jīng)成為標(biāo)配撩荣,這時每個處理器都有自己的緩存铣揉,這就涉及到了緩存一致性的問題,CPU有不同強弱的一致性模型餐曹,最強的一致性安全性最高老速,也符合我們的順序思考的模式,但是在性能上因為需要不同CPU之間的協(xié)調(diào)通信就會有很多開銷凸主。

典型的CPU緩存結(jié)構(gòu)示意圖如下

CPU的指令周期通常為取指令橘券、解析指令讀取數(shù)據(jù)、執(zhí)行指令卿吐、數(shù)據(jù)寫回寄存器或內(nèi)存旁舰。串行執(zhí)行指令時其中的讀取存儲數(shù)據(jù)部分占用時間較長,所以CPU普遍采取指令流水線的方式同時執(zhí)行多個指令, 提高整體吞吐率嗡官,就像工廠流水線一樣箭窜。

讀取數(shù)據(jù)和寫回數(shù)據(jù)到內(nèi)存相比執(zhí)行指令的速度不在一個數(shù)量級上,所以CPU使用寄存器衍腥、高速緩存作為緩存和緩沖磺樱,在從內(nèi)存中讀取數(shù)據(jù)時,會讀取一個緩存行(cache line)的數(shù)據(jù)(類似磁盤讀取讀取一個block)婆咸。數(shù)據(jù)寫回的模塊在舊數(shù)據(jù)沒有在緩存中的情況下會將存儲請求放入一個store buffer中繼續(xù)執(zhí)行指令周期的下一個階段竹捉,如果存在于緩存中則會更新緩存,緩存中的數(shù)據(jù)會根據(jù)一定策略flush到內(nèi)存尚骄。

上面這段代碼執(zhí)行時我們可能認為count = 1會在stop = false前執(zhí)行完成块差,這在上面的CPU執(zhí)行圖中顯示的理想狀態(tài)下是正確的,但是要考慮上寄存器倔丈、緩存緩沖的時候就不正確了, 例如stop本身在緩存中但是count不在憨闰,則可能stop更新后再count的write buffer寫回之前刷新到了內(nèi)存。

另外CPU需五、編譯器(對于Java一般指JIT)都可能會修改指令執(zhí)行順序鹉动,例如上述代碼中count = 1和stop = false兩者并沒有依賴關(guān)系,所以CPU宏邮、編譯器都有可能修改這兩者的順序泽示,而在單線程執(zhí)行的程序看來結(jié)果是一樣的,這也是CPU蜀铲、編譯器要保證的as-if-serial(不管如何修改執(zhí)行順序边琉,單線程的執(zhí)行結(jié)果不變)。由于很大部分程序執(zhí)行都是單線程的记劝,所以這樣的優(yōu)化是可以接受并且?guī)砹溯^大的性能提升变姨。但是在多線程的情況下,如果沒有進行必要的同步操作則可能會出現(xiàn)令人意想不到的結(jié)果厌丑。例如在線程T1執(zhí)行完initCountAndStop方法后定欧,線程T2執(zhí)行printResult渔呵,得到的可能是0, false, 可能是1, false, 也可能是0, true。如果線程T1先執(zhí)行doLoop()砍鸠,線程T2一秒后執(zhí)行initCountAndStop, 則T1可能會跳出循環(huán)扩氢、也可能由于編譯器的優(yōu)化永遠無法看到stop的修改。

由于上述這些多線程情況下的各種問題爷辱,多線程中的程序順序已經(jīng)不是底層機制中的執(zhí)行順序和結(jié)果录豺,編程語言需要給開發(fā)者一種保證,這個保證簡單來說就是一個線程的修改何時對其他線程可見饭弓,因此Java語言提出了JavaMemoryModel即Java內(nèi)存模型双饥,對于Java語言、JVM弟断、編譯器等實現(xiàn)者需要按照這個模型的約定來進行實現(xiàn)咏花。Java提供了Volatile、synchronized阀趴、final等機制來幫助開發(fā)者保證多線程程序在所有處理器平臺上的正確性昏翰。

在JDK1.5之前,Java的內(nèi)存模型有著嚴(yán)重的問題刘急,例如在舊的內(nèi)存模型中棚菊,一個線程可能在構(gòu)造器執(zhí)行完成后看到一個final字段的默認值、volatile字段的寫入可能會和非volatile字段的讀寫重排序排霉。

所以在JDK1.5中窍株,通過JSR133提出了新的內(nèi)存模型,修復(fù)之前出現(xiàn)的問題攻柠。

重排序規(guī)則

volatile和監(jiān)視器鎖


其中普通讀指getfield, getstatic, 非volatile數(shù)組的arrayload, 普通寫指putfield, putstatic, 非volatile數(shù)組的arraystore。

volatile讀寫分別是volatile字段的getfield, getstatic和putfield, putstatic后裸。

monitorenter是進入同步塊或同步方法,monitorexist指退出同步塊或同步方法瑰钮。

上述表格中的No指先后兩個操作不允許重排序,如(普通寫, volatile寫)指非volatile字段的寫入不能和之后任意的volatile字段的寫入重排序微驶。當(dāng)沒有No時浪谴,說明重排序是允許的,但是JVM需要保證最小安全性-讀取的值要么是默認值因苹,要么是其他線程寫入的(64位的double和long讀寫操作是個特例苟耻,當(dāng)沒有volatile修飾時,并不能保證讀寫是原子的扶檐,底層可能將其拆分為兩個單獨的操作)凶杖。

final字段

final字段有兩個額外的特殊規(guī)則

final字段的寫入(在構(gòu)造器中進行)以及final字段對象本身的引用的寫入都不能和后續(xù)的(構(gòu)造器外的)持有該final字段的對象的寫入重排序。例如, 下面的語句是不能重排序的

x.finalField?=?v;?...;?sharedRef?=?x;

final字段的第一次加載不能和持有這個final字段的對象的寫入重排序款筑,例如下面的語句是不允許重排序的

x?=?sharedRef;?...;?i?=?x.finalField

內(nèi)存屏障

處理器都支持一定的內(nèi)存屏障(memory barrier)或柵欄(fence)來控制重排序和數(shù)據(jù)在不同的處理器間的可見性智蝠。例如腾么,CPU將數(shù)據(jù)寫回時,會將store請求放入write buffer中等待flush到內(nèi)存杈湾,可以通過插入barrier的方式防止這個store請求與其他的請求重排序解虱、保證數(shù)據(jù)的可見性∑嶙玻可以用一個生活中的例子類比屏障殴泰,例如坐地鐵的斜坡式電梯時,大家按順序進入電梯浮驳,但是會有一些人從左側(cè)繞過去艰匙,這樣出電梯時順序就不相同了,如果有一個人攜帶了一個大的行李堵住了(屏障)抹恳,則后面的人就不能繞過去了:)员凝。另外這里的barrier和GC中用到的write barrier是不同的概念。

在此我向大家推薦一個架構(gòu)學(xué)習(xí)交流群奋献。交流學(xué)習(xí)群號:478030634 里面會分享一些資深架構(gòu)師錄制的視頻錄像:有Spring健霹,MyBatis,Netty源碼分析瓶蚂,高并發(fā)糖埋、高性能、分布式窃这、微服務(wù)架構(gòu)的原理瞳别,JVM性能優(yōu)化、分布式架構(gòu)等這些成為架構(gòu)師必備的知識體系杭攻。還能領(lǐng)取免費的學(xué)習(xí)資源祟敛,目前受益良多

內(nèi)存屏障的分類

幾乎所有的處理器都支持一定粗粒度的barrier指令,通常叫做Fence(柵欄兆解、圍墻)馆铁,能夠保證在fence之前發(fā)起的load和store指令都能嚴(yán)格的和fence之后的load和store保持有序。通常按照用途會分為下面四種barrier

LoadLoad Barriers

Load1; LoadLoad; Load2;

保證Load1的數(shù)據(jù)在Load2及之后的load前加載

StoreStore Barriers

Store1; StoreStore; Store2

保證Store1的數(shù)據(jù)先于Store2及之后的數(shù)據(jù) 在其他處理器可見

LoadStore Barriers

Load1; LoadStore; Store2

保證Load1的數(shù)據(jù)的加載在Store2和之后的數(shù)據(jù)flush前

StoreLoad Barriers

Store1; StoreLoad; Load2

保證Store1的數(shù)據(jù)在其他處理器前可見(如flush到內(nèi)存)先于Load2和之后的load的數(shù)據(jù)的加載锅睛。StoreLoad Barrier能夠防止load讀取到舊數(shù)據(jù)而不是最近其他處理器寫入的數(shù)據(jù)埠巨。

幾乎近代的所有的多處理器都需要StoreLoad,StoreLoad的開銷通常是最大的现拒,并且StoreLoad具有其他三種屏障的效果辣垒,所以StoreLoad可以當(dāng)做一個通用的(但是更高開銷的)屏障。

所以印蔬,利用上述的內(nèi)存屏障勋桶,可以實現(xiàn)上面表格中的重排序規(guī)則

為了支持final字段的規(guī)則,需要對final的寫入增加barrier

x.finalField = v; StoreStore; sharedRef = x;

插入內(nèi)存屏障

基于上面的規(guī)則,可以在volatile字段哥遮、synchronized關(guān)鍵字的處理上增加屏障來滿足內(nèi)存模型的規(guī)則

volatile store前插入StoreStore屏障

所有final字段寫入后但在構(gòu)造器返回前插入StoreStore

volatile store后插入StoreLoad屏障

在volatile load后插入LoadLoad和LoadStore屏障

monitor enter和volatile load規(guī)則一致岂丘,monitor exit 和volatile store規(guī)則一致。

HappenBefore

前面提到的各種內(nèi)存屏障對應(yīng)開發(fā)者來說還是比較復(fù)雜底層眠饮,因此JMM又可以使用一系列HappenBefore的偏序關(guān)系的規(guī)則方式來說明奥帘,要想保證執(zhí)行操作B的線程看到操作A的結(jié)果(無論A和B是否在同一個線程中執(zhí)行), 那么在A和B之間必須要滿足HappenBefore關(guān)系,否則JVM可以對它們?nèi)我庵嘏判颉?/p>

HappenBefore規(guī)則列表

HappendBefore規(guī)則包括

程序順序規(guī)則: 如果程序中操作A在操作B之前仪召,那么同一個線程中操作A將在操作B之前進行

監(jiān)視器鎖規(guī)則: 在監(jiān)視器鎖上的鎖操作必須在同一個監(jiān)視器鎖上的加鎖操作之前執(zhí)行

volatile變量規(guī)則: volatile變量的寫入操作必須在該變量的讀操作之前執(zhí)行

線程啟動規(guī)則: 在線程上對Thread.start的調(diào)用必須在該線程中執(zhí)行任何操作之前執(zhí)行

線程結(jié)束規(guī)則: 線程中的任何操作都必須在其他線程檢測到該線程已經(jīng)結(jié)束之前執(zhí)行

中斷規(guī)則: 當(dāng)一個線程在另一個線程上調(diào)用interrupt時寨蹋,必須在被中斷線程檢測到interrupt之前執(zhí)行

傳遞性: 如果操作A在操作B之前執(zhí)行,并且操作B在操作C之前執(zhí)行扔茅,那么操作A在操作C之前執(zhí)行已旧。

其中顯示鎖與監(jiān)視器鎖有相同的內(nèi)存語義,原子變量與volatile有相同的內(nèi)存語義召娜。鎖的獲取和釋放运褪、volatile變量的讀取和寫入操作滿足全序關(guān)系,所以可以使用volatile的寫入在后續(xù)的volatile的讀取之前進行玖瘸。

可以利用上述HappenBefore的多個規(guī)則進行組合秸讹。

例如線程A進入監(jiān)視器鎖后,在釋放監(jiān)視器鎖之前的操作根據(jù)程序順序規(guī)則HappenBefore于監(jiān)視器釋放操作雅倒,而監(jiān)視器釋放操作HappenBefore于后續(xù)的線程B的對相同監(jiān)視器鎖的獲取操作璃诀,獲取操作HappenBefore與線程B中的操作。

在此我向大家推薦一個架構(gòu)學(xué)習(xí)交流群蔑匣。交流學(xué)習(xí)群號:478030634 ?里面會分享一些資深架構(gòu)師錄制的視頻錄像:有Spring劣欢,MyBatis,Netty源碼分析裁良,高并發(fā)凿将、高性能、分布式趴久、微服務(wù)架構(gòu)的原理丸相,JVM性能優(yōu)化、分布式架構(gòu)等這些成為架構(gòu)師必備的知識體系彼棍。還能領(lǐng)取免費的學(xué)習(xí)資源,目前受益良多

大家覺得文章對你還是有一點點幫助的膳算,大家可以點擊下方二維碼進行關(guān)注座硕。《Java爛豬皮》公眾號聊的不僅僅是Java技術(shù)知識涕蜂,還有面試等干貨华匾,后期還有大量架構(gòu)干貨。大家一起關(guān)注吧!關(guān)注爛豬皮蜘拉,你會了解的更多..............

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末萨西,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子旭旭,更是在濱河造成了極大的恐慌谎脯,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,639評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件持寄,死亡現(xiàn)場離奇詭異源梭,居然都是意外死亡,警方通過查閱死者的電腦和手機稍味,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評論 3 385
  • 文/潘曉璐 我一進店門废麻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人模庐,你說我怎么就攤上這事烛愧。” “怎么了掂碱?”我有些...
    開封第一講書人閱讀 157,221評論 0 348
  • 文/不壞的土叔 我叫張陵土涝,是天一觀的道長。 經(jīng)常有香客問我垫卤,道長甥厦,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,474評論 1 283
  • 正文 為了忘掉前任悴了,我火速辦了婚禮搏恤,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘湃交。我一直安慰自己熟空,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,570評論 6 386
  • 文/花漫 我一把揭開白布搞莺。 她就那樣靜靜地躺著息罗,像睡著了一般。 火紅的嫁衣襯著肌膚如雪才沧。 梳的紋絲不亂的頭發(fā)上迈喉,一...
    開封第一講書人閱讀 49,816評論 1 290
  • 那天,我揣著相機與錄音温圆,去河邊找鬼挨摸。 笑死,一個胖子當(dāng)著我的面吹牛岁歉,可吹牛的內(nèi)容都是我干的得运。 我是一名探鬼主播,決...
    沈念sama閱讀 38,957評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼熔掺!你這毒婦竟也來了饱搏?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,718評論 0 266
  • 序言:老撾萬榮一對情侶失蹤置逻,失蹤者是張志新(化名)和其女友劉穎推沸,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體诽偷,經(jīng)...
    沈念sama閱讀 44,176評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡坤学,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,511評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了报慕。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片深浮。...
    茶點故事閱讀 38,646評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖眠冈,靈堂內(nèi)的尸體忽然破棺而出飞苇,到底是詐尸還是另有隱情,我是刑警寧澤蜗顽,帶...
    沈念sama閱讀 34,322評論 4 330
  • 正文 年R本政府宣布布卡,位于F島的核電站,受9級特大地震影響雇盖,放射性物質(zhì)發(fā)生泄漏忿等。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,934評論 3 313
  • 文/蒙蒙 一崔挖、第九天 我趴在偏房一處隱蔽的房頂上張望贸街。 院中可真熱鬧,春花似錦狸相、人聲如沸薛匪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽逸尖。三九已至,卻和暖如春瘸右,著一層夾襖步出監(jiān)牢的瞬間娇跟,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評論 1 266
  • 我被黑心中介騙來泰國打工太颤, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留逞频,地道東北人。 一個月前我還...
    沈念sama閱讀 46,358評論 2 360
  • 正文 我出身青樓栋齿,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子瓦堵,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,514評論 2 348

推薦閱讀更多精彩內(nèi)容