Java內存模型基礎

Java內存模型的基礎

并發(fā)編程模型的兩個關鍵問題

在并發(fā)編程種,需要處理兩個關鍵問題:線程之間如何通信及線程之間如何同步(這里的線程是指并發(fā)執(zhí)行的活動實體)蕾久。通信是指線程之間以何種機制來交換信息靴寂。在命令式編程種蛾魄,線程之間的通信機制有兩種:共享內存和消息傳遞征峦。

在共享內存的并發(fā)模型里情屹,線程之間共享程序的公共狀態(tài)柏锄,通過寫-讀內存中的公共狀態(tài)進行隱式通信酿箭。在消息傳遞的并發(fā)模型里,線程之間沒有公共狀態(tài)趾娃,線程之間必須通過發(fā)送消息來顯式進行通信缭嫡。

同步是指程序中用于控制不同線程間操作發(fā)生相對順序的機制。在共享內存并發(fā)模型里抬闷,同步是閑時間進行的妇蛀。程序員必須顯式指定某個方法或某段代碼需要在線程之間互斥執(zhí)行。在消息傳遞的并發(fā)模型里笤成,由于消息的發(fā)送必須在消息的接受之前评架,因此同步是隱式進行的。

Java的并發(fā)采用的是共享內存模型炕泳,Java線程之間的通信總是隱式進行纵诞,整個通信過程對程序員完全透明。如果編寫多線程程序的Java程序員不理解隱式進行的線程之間通信的工作機制培遵,很可能會遇到各種奇怪的內存可見性問題挣磨。

Java內存模型的抽象結構

Java線程之間的通信由Java內存模型(簡稱為JMM)控制,JMM決定一個線程對共享變量的寫入何時對另一個線程可見荤懂。

Java內存模型的抽象示意圖如圖所示:


229.png

如果線程A與線程B之間要通信的話茁裙,必須要經歷下面2個步驟:

1)線程A把本地內存A中更新過的共享變量刷新到主內存中去。

2)線程B到主內存中去讀取線程A之前已更新過的共享變量节仿。

下面通過示意圖來說明這兩個步驟:

230.png

如圖所示晤锥,本地內存A和本地內存B由主內存中共享變量x的副本。假設初始時廊宪,這3個內存中的x值都為0矾瘾。線程A在執(zhí)行時,把更新后的x值(假設值為1)臨時存放在自己的本地內存A中箭启。當線程A和線程B需要通信時壕翩,線程A首先會把自己本地內存中修改后的x值刷新到主內存中,此時主內存中的x值變?yōu)榱?.隨后傅寡,線程B到主內存中讀取線程A更新后的x值放妈,此時線程B的本地內存的x值也變?yōu)榱?北救。

從整體來看,這兩個步驟實質上是線程A在向線程B發(fā)送消息芜抒,而且這個通信過程必須要經過主內存珍策。JMM通過控制主內存與每個線程的本地內存之間的交互,來為Java程序提供內存可見性保證宅倒。

從源代碼到指令序列的重排序

在執(zhí)行程序時攘宙,為了提高性能,編譯器和處理器常常會對指令做重排序拐迁。重排序分3種類型蹭劈。

  1. 編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語義的前提下线召,可以重新安排語句的執(zhí)行順序链方。

  2. 指令級并行的重排序。現(xiàn)代處理器采用了指令級并行技術(Instruction-Level Parallelism灶搜,ILP)來講多條指令重疊執(zhí)行。如果不存在數據依賴性工窍,處理器可以改變語句對應機器指令的執(zhí)行順序割卖。

  3. 內存系統(tǒng)的重排序。由于處理器使用緩存和讀/寫緩沖區(qū)患雏,這使得加載和存儲操作看上去可能是在亂序執(zhí)行鹏溯。

從Java源代碼到最終實際執(zhí)行的指令序列,會分別經歷下面3種重排序淹仑,如圖所示:

231.png

上述的1屬于編譯器重排序丙挽,2和3屬于處理器重排序。

并發(fā)編程模型的分類

現(xiàn)在的處理器使用寫緩沖區(qū)臨時保存向內存寫入的數據匀借。寫緩沖區(qū)可以保證指令流水線持續(xù)運行颜阐,它可以避免由于處理器停頓下來等待向內存寫入數據而產生的延遲。同時吓肋,通過以批處理的方式刷新寫緩沖區(qū)凳怨,以及合并寫緩沖區(qū)中對同一內存地址的多次寫,減少對內存總線的占用是鬼。雖然寫緩存區(qū)有這么多好處肤舞,但每個處理器上的寫緩沖區(qū),僅僅對它所在的處理器可見均蜜。這個特性會對內存操作的執(zhí)行順序產生重要的影響:處理器對內存的讀/寫操作的執(zhí)行順序李剖,不一定與內存實際發(fā)生的讀/寫操作順序一致!為了具體說明囤耳,請看下面的圖:

232.png

假設處理器A和處理器B按程序的順序并執(zhí)行內存訪問篙顺,最終可能得到x=y=0的結果偶芍。

233.png

這里處理器A和處理器B可以同時把共享變量寫入自己的寫緩沖區(qū)(A1,B1)慰安,然后從內存中讀取另一個共享變量(A2腋寨,B2),最后才把自己寫緩存區(qū)中保存的臟數據刷新到內存中(A3化焕,B3)萄窜。當以這種時序執(zhí)行時,程序就可以得到x=y=0的結果撒桨。

從內存操作實際發(fā)生的順序來看查刻,直到處理器A執(zhí)行A3來刷新自己的寫緩存區(qū),寫操作A1才算真正執(zhí)行了凤类。雖然處理器A執(zhí)行內存操作的順序為A1->A2穗泵,但內存操作實際發(fā)生的順序卻是A2->A1。此時谜疤,處理器A的內存操作順序被重排序了(處理器B的情況和處理器A一樣這里就不贅述了)佃延。


注意

  • sparc_TSO是指以TSO(Total Store Order)內存模型運行時sparc處理器的特性。
  • 表中的X86包含X64及AMD64夷磕。
  • 由于ARM處理器的內存模型與PowerPC處理器的內存模型非常類似履肃,本文將忽略它。

為了保證內存可見性坐桩,Java編譯器在生成指令序列的適當位置會插入內存屏障指令來禁止特定類型的處理器重排序尺棋。JMM把內存屏障指令來禁止指令分為四類,如表所示:

屏障類型 指令實例 說明
LoadLoad Barriers Load1;LoadLoad;Load2 確保Load1數據的裝載先于Load2及所有后續(xù)裝載指令的裝載
StoreStore Barriers Store1;StoreStore;Store2 確保Store1數據對其他處理器可見(刷新到內存)先于Store2及所有后續(xù)存儲指令的存儲
LoadStore Barriers Load1;LoadStroe;Store2 確保Load1數據裝載先于Store2及所有后續(xù)的存儲指令刷新到內存
StoreLoad Barriers Store1;StoreLoad;Load2 確保Store1數據對其他處理器變得可見(指刷新到內存)先于Load2及所有后續(xù)裝載指令的裝載绵跷。StoreLoad Barriers會使該屏障之前的所有內存訪問指令(存儲和裝載指令)完成之后膘螟,才執(zhí)行該屏障之后的內存訪問指令。

StoreLoad Barriers是一個“全能型”的屏障,它同時具有其他3個屏障的效果。現(xiàn)代的多處理器大多支持該屏障(其他類型的屏障不一定被所有處理器支持)峭沦。執(zhí)行該屏障開銷會很昂貴忠藤,因為當前處理器通常要把寫緩沖區(qū)中的數據全部刷新到內存中(Buffer Fuller Flush)。

happens-before簡介

從JDK5開始,Java使用新的JSR-133內存模型(除非特別說明,本文針對的都是JSR-133內存模型)。JSR-133使用happens-before的概念來闡述操作之間的內存可見性嘿期。在JMM中,如果一個操作執(zhí)行的結果需要對另一個操作可見埋合,那么這兩個操作之間必須要存在happens-before關系备徐。這里提到的兩個操作既可以是在一個線程之內,也可以是在不同線程之間甚颂。

與程序員密切相關的happens-before規(guī)則如下蜜猾。

  • 程序順序規(guī)則:一個線程中的每個操作秀菱,happens-before于該線程中的任意后續(xù)操作。
  • 監(jiān)視器鎖規(guī)則:對一個鎖的解鎖蹭睡,happens-before于隨后對這個鎖的加鎖衍菱。
  • volatile變量規(guī)則:對一個volatile域的寫,happens-before于任意后續(xù)對這個volatile域的讀肩豁。
  • 傳遞性:如果A happens-before B脊串,且B happens-before C,那么A happens-before C清钥。

注意 兩個操作之間具有happens-before關系琼锋,并不意味著前一個操作必須要在后一個操作之前執(zhí)行!happens-before僅僅要求前一個操作(執(zhí)行的結果)對后一個操作可見祟昭,且前一個操作按順序排在第二個操作之前(this first is visible to and ordered before the second)缕坎。

happens-before與JMM的關系如圖所示:

234.png

如圖所示,一個happens-before規(guī)則對應于一個或多個編譯器和處理器重排序規(guī)則篡悟。對于Java程序員來說谜叹,happens-before規(guī)則簡單易懂,它避免Java程序員為了理解JMM提供的內存可見性保證而去學習復雜的重排序規(guī)則以及這些規(guī)則的具體實現(xiàn)方法搬葬。

重排序

重排序是指編譯器和處理器為了優(yōu)化程序性能而對指令序列進行重新排序的一種手段荷腊。

數據依賴性

如果兩個操作訪問同一個變量,且這兩個操作中有一個為寫操作踩萎,此時這兩個操作之間就存在數據依賴性。數據以來分為下列3種類型很钓。

名稱 代碼示例 說明
寫后讀 a=1; b=a; 寫一個變量之后香府,再讀這個位置
寫后寫 a=1; a=2; 寫一個變量之后,再寫這個變量
讀后寫 a=b; b=1; 讀一個變量之后码倦,再寫這個變量

as-if-serial語義

as-if-serial語義的意思是:不管怎么重排序(編譯器和處理器為了提高并行度)企孩,(單線程)程序的執(zhí)行結果不能被改變。編譯器袁稽、runtime和處理器都必須遵守as-if-serial語義勿璃。

為了遵守as-if-serial語義,編譯器和處理器不會對存在數據依賴關系的操作做重排序推汽,因為這種重排序會改變執(zhí)行結果补疑。

順序一致性

數據競爭與順序一致性

Java內存模型規(guī)范對數據競爭的定義如下。

在一個線程中寫一個變量歹撒,

在另一個線程讀同一個變量莲组,

而且寫和讀沒有通過同步來排序。

如果一個多線程程序能夠正確同步暖夭,這個程序將是一個沒有數據競爭的程序锹杈。

如果程序是正確同步的撵孤,程序的執(zhí)行講具有順序一致性(Sequentially Consistent)——即程序的執(zhí)行結果與該程序在順序一致性內存模型中的執(zhí)行結果相同。這里的同步是指廣義上的同步竭望,包括對常用同步原語(synchronized邪码、volatile和final)的正確使用。

順序一致性內存模型

順序一致性內存模型有兩大特性咬清。

1)一個線程中的所有操作必須按照程序的順序來執(zhí)行闭专。

2)(不管程序是否同步)所有現(xiàn)場都只能看到一個單一的操作執(zhí)行順序。在順序一致性內存模型中枫振,每個操作都必須源自執(zhí)行且立刻對所有線程可見喻圃。

順序一致性模型中,所有操作完全按程序的順序串行執(zhí)行粪滤。

未同步程序的執(zhí)行特性

未同步程序在兩個模型中的執(zhí)行特性有如下幾個差異斧拍。

1)順序一致性模型保證單線程內的操作會按程序的順序執(zhí)行,而JMM不保證單線程內的操作會按程序的順序執(zhí)行

2)順序一致性模型保證所有線程只能看到一致的操作執(zhí)行順序杖小,而JMM不保證所有線程能看到一致的操作執(zhí)行順序肆汹。

3)JMM不保證對64位的long型和double型變量的寫操作具有原子性,而順序一致性模型保證對所有的內存讀/寫操作都具有原子操作

在計算機中予权,數據通過總線在處理器和內存之間傳遞昂勉。每次處理器和內存之間的數據傳遞都是通過一系列步驟來完成的,這一系列步驟稱之為總線事務( Bus Transaction)扫腺「谡眨總線事務包括讀事務(Read Transaction)和寫事務(Write Transaction)。讀事務從內存?zhèn)魉蛿祿教幚砥靼驶罚瑢懯聞諒奶幚砥鱾魉蛿祿絻却嬖苤粒總€事務會讀/寫內存中一個或多個物理上來連續(xù)的字。這里的關鍵是躁劣,總線會同步視圖并發(fā)使用總線的事務迫吐。在一個處理器執(zhí)行總線事務期間,總線會禁止其他的處理器和I/O設備執(zhí)行內存的讀/寫账忘。

在任意時間點志膀,最多只能有一個處理器可以訪問內存。這個特性確保了單個總線事務之中的內存讀/寫操作具有原子性鳖擒。

一些32位的處理器上溉浙,如果要求對64位數據的寫操作具有原子性,會有比較大的開銷蒋荚。當JMM在這種處理器上運行時放航,可能會把一個64位long/double型變量的寫操作拆分為兩個32位的寫操作來執(zhí)行。這兩個32位的寫操作可能會被分配到不同的總線事務中執(zhí)行圆裕,此時對這個64位變量的寫操作將不具有原子性广鳍。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末荆几,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子赊时,更是在濱河造成了極大的恐慌吨铸,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,743評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件祖秒,死亡現(xiàn)場離奇詭異诞吱,居然都是意外死亡,警方通過查閱死者的電腦和手機竭缝,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評論 3 385
  • 文/潘曉璐 我一進店門房维,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人抬纸,你說我怎么就攤上這事咙俩。” “怎么了湿故?”我有些...
    開封第一講書人閱讀 157,285評論 0 348
  • 文/不壞的土叔 我叫張陵阿趁,是天一觀的道長。 經常有香客問我坛猪,道長脖阵,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,485評論 1 283
  • 正文 為了忘掉前任墅茉,我火速辦了婚禮命黔,結果婚禮上,老公的妹妹穿的比我還像新娘就斤。我一直安慰自己悍募,他們只是感情好,可當我...
    茶點故事閱讀 65,581評論 6 386
  • 文/花漫 我一把揭開白布战转。 她就那樣靜靜地躺著搜立,像睡著了一般以躯。 火紅的嫁衣襯著肌膚如雪槐秧。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,821評論 1 290
  • 那天忧设,我揣著相機與錄音刁标,去河邊找鬼。 笑死址晕,一個胖子當著我的面吹牛膀懈,可吹牛的內容都是我干的。 我是一名探鬼主播谨垃,決...
    沈念sama閱讀 38,960評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼启搂,長吁一口氣:“原來是場噩夢啊……” “哼硼控!你這毒婦竟也來了?” 一聲冷哼從身側響起胳赌,我...
    開封第一講書人閱讀 37,719評論 0 266
  • 序言:老撾萬榮一對情侶失蹤牢撼,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后疑苫,有當地人在樹林里發(fā)現(xiàn)了一具尸體熏版,經...
    沈念sama閱讀 44,186評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,516評論 2 327
  • 正文 我和宋清朗相戀三年捍掺,在試婚紗的時候發(fā)現(xiàn)自己被綠了撼短。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,650評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡挺勿,死狀恐怖曲横,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情满钟,我是刑警寧澤胜榔,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站湃番,受9級特大地震影響夭织,放射性物質發(fā)生泄漏。R本人自食惡果不足惜吠撮,卻給世界環(huán)境...
    茶點故事閱讀 39,936評論 3 313
  • 文/蒙蒙 一尊惰、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧泥兰,春花似錦弄屡、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至削彬,卻和暖如春全庸,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背融痛。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評論 1 266
  • 我被黑心中介騙來泰國打工壶笼, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人雁刷。 一個月前我還...
    沈念sama閱讀 46,370評論 2 360
  • 正文 我出身青樓覆劈,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子责语,可洞房花燭夜當晚...
    茶點故事閱讀 43,527評論 2 349

推薦閱讀更多精彩內容

  • 基礎 并發(fā)編程的模型分類 在并發(fā)編程需要處理的兩個關鍵問題是:線程之間如何通信 和 線程之間如何同步炮障。 通信 通信...
    GeekerLou閱讀 537評論 0 0
  • 1.Java內存模型的基礎 ①并發(fā)編程模型的兩個關鍵問題 線程之間如何通信、線程之間如何同步 通信是指線程之間以何...
    加夕閱讀 734評論 0 1
  • Java的并發(fā)采用的是共享內存模型(而非消息傳遞模型)坤候,線程之間共享程序的公共狀態(tài)铝阐,線程之間通過寫-讀內存中的公共...
    阿斯蒂芬2閱讀 471評論 0 1
  • 今晚老公不在,我和公婆共進晚餐铐拐。 一向不多言的公公突然說起了老公小時候的事情徘键。老公初三患上了急性視神經炎...
    王樂呵閱讀 419評論 1 5
  • 增強政治意識、大局意識遍蟋、核心意識吹害、看齊意識 ...
    尼丹閱讀 166評論 0 0