5分鐘讓你明白Java內(nèi)存模型

Question:在并發(fā)編程中燥狰,多個線程之間采取什么機(jī)制進(jìn)行通信(信息交換)暑诸,什么機(jī)制進(jìn)行數(shù)據(jù)的同步?

Answer:在Java語言中芯义,采用的是共享內(nèi)存模型來實現(xiàn)多線程之間的信息交換和數(shù)據(jù)同步的愕难。

線程之間通過共享程序公共的狀態(tài)早龟,通過讀-寫內(nèi)存中公共狀態(tài)的方式來進(jìn)行隱式的通信惫霸。同步指的是程序在控制多個線程之間執(zhí)行程序的相對順序的機(jī)制,在共享內(nèi)存模型中拄衰,同步是顯式的,程序員必須顯式指定某個方法/代碼塊需要在多線程之間互斥執(zhí)行饵骨。

概述

Java內(nèi)存模型的主要目標(biāo)是定義程序中各個變量的訪問規(guī)則翘悉,即在JVM中將變量存儲到內(nèi)存和從內(nèi)存中取出變量這樣的底層細(xì)節(jié)。此處的變量與Java編程里面的變量有所不同居触,它包含了實例字段妖混、靜態(tài)字段和構(gòu)成數(shù)組對象的元素,但不包含局部變量和方法參數(shù)轮洋,因為后者是線程私有的制市,不會共享,當(dāng)然不存在數(shù)據(jù)競爭問題(如果局部變量是一個reference引用類型弊予,它引用的對象在Java堆中可被各個線程共享祥楣,但是reference引用本身在Java棧的局部變量表中,是線程私有的)汉柒。為了獲得較高的執(zhí)行效能误褪,Java內(nèi)存模型并沒有限制執(zhí)行引起使用處理器的特定寄存器或者緩存來和主內(nèi)存進(jìn)行交互,也沒有限制即時編譯器進(jìn)行調(diào)整代碼執(zhí)行順序這類優(yōu)化措施碾褂。

JMM規(guī)定了所有的變量都存儲在主內(nèi)存(Main Memory)中兽间。每個線程還有自己的工作內(nèi)存(Working Memory),線程的工作內(nèi)存中保存了該線程使用到的變量的主內(nèi)存的副本拷貝,線程對變量的所有操作(讀取正塌、賦值等)都必須在工作內(nèi)存中進(jìn)行嘀略,而不能直接讀寫主內(nèi)存中的變量(volatile變量仍然有工作內(nèi)存的拷貝,但是由于它特殊的操作順序性規(guī)定乓诽,所以看起來如同直接在主內(nèi)存中讀寫訪問一般)帜羊。不同的線程之間也無法直接訪問對方工作內(nèi)存中的變量,線程之間值的傳遞都需要通過主內(nèi)存來完成鸠天。

線程1和線程2要想進(jìn)行數(shù)據(jù)的交換一般要經(jīng)歷下面的步驟:

1.線程1把工作內(nèi)存1中的更新過的共享變量刷新到主內(nèi)存中去逮壁。

2.線程2到主內(nèi)存中去讀取線程1刷新過的共享變量,然后copy一份到工作內(nèi)存2中去粮宛。

內(nèi)存模型的特性

Java內(nèi)存模型是圍繞著并發(fā)編程中原子性窥淆、可見性、有序性這三個特征來建立的巍杈,那我們依次看一下這三個特征:

原子性(Atomicity)

原子性是指一個操作不能被打斷忧饭,要么全部執(zhí)行完畢,要么不執(zhí)行筷畦。在這點(diǎn)上有點(diǎn)類似于事務(wù)操作词裤,要么全部執(zhí)行成功刺洒,要么回退到執(zhí)行該操作之前的狀態(tài)。

基本類型數(shù)據(jù)的訪問大都是原子操作吼砂,long 和double類型的變量是64位逆航,但是在32位JVM中,32位的JVM會將64位數(shù)據(jù)的讀寫操作分為2次32位的讀寫操作來進(jìn)行渔肩,這就導(dǎo)致了long因俐、double類型的變量在32位虛擬機(jī)中是非原子操作,數(shù)據(jù)有可能會被破壞周偎,也就意味著多個線程在并發(fā)訪問的時候是線程非安全的抹剩。

在這里我推薦下我自己的JAVAqun:479121291,不管你是小白還是大牛蓉坎,小編我都挺歡迎澳眷,不定期分享干貨,包括我自己整理的一份最新JAVA資料和零基礎(chǔ)入門教程蛉艾!钳踊,歡迎初學(xué)和進(jìn)階中的小伙伴。

下面我們來演示這個32位JVM下勿侯,對64位long類型的數(shù)據(jù)的訪問的問題:

【代碼1】

我們創(chuàng)建了4個線程來對long類型的變量t進(jìn)行賦值箍土,賦值分別為100,200,-300罐监,-400吴藻,有一個線程負(fù)責(zé)讀取變量t,如果正常的話,讀取到的t的值應(yīng)該是我們賦值中的一個弓柱,但是在32的JVM中(ps: 64位的就別想了)沟堡,事情會出乎預(yù)料。如果程序正常的話矢空,我們控制臺不會有任何的輸出航罗,可實際上,程序一運(yùn)行屁药,控制臺就輸出了下面的信息:

之所以會出現(xiàn)上面的情況粥血,是因為在32位JVM中,64位的long數(shù)據(jù)的讀和寫都不是原子操作酿箭,即不具有原子性复亏,并發(fā)的時候相互干擾了。

32位的JVM中缭嫡,要想保證對long缔御、double類型數(shù)據(jù)的操作的原子性,可以對訪問該數(shù)據(jù)的方法進(jìn)行同步妇蛀,就像下面的:

【代碼2】

這樣做的話耕突,可以保證對64位數(shù)據(jù)操作的原子性笤成。

可見性

一個線程對共享變量做了修改之后,其他的線程立即能夠看到(感知到)該變量這種修改(變化)眷茁。

Java內(nèi)存模型是通過將在工作內(nèi)存中的變量修改后的值同步到主內(nèi)存炕泳,在讀取變量前從主內(nèi)存刷新最新值到工作內(nèi)存中,這種依賴主內(nèi)存的方式來實現(xiàn)可見性的上祈。

無論是普通變量還是volatile變量都是如此培遵,區(qū)別在于:volatile的特殊規(guī)則保證了volatile變量值修改后的新值立刻同步到主內(nèi)存,每次使用volatile變量前立即從主內(nèi)存中刷新雇逞,因此volatile保證了多線程之間的操作變量的可見性荤懂,而普通變量則不能保證這一點(diǎn)茁裙。

除了volatile關(guān)鍵字能實現(xiàn)可見性之外塘砸,還有synchronized,Lock,final也是可以的晤锥。

使用synchronized關(guān)鍵字掉蔬,在同步方法/同步塊開始時(Monitor Enter),使用共享變量時會從主內(nèi)存中刷新變量值到工作內(nèi)存中(即從主內(nèi)存中讀取最新值到線程私有的工作內(nèi)存中),在同步方法/同步塊結(jié)束時(Monitor Exit),會將工作內(nèi)存中的變量值同步到主內(nèi)存中去(即將線程私有的工作內(nèi)存中的值寫入到主內(nèi)存進(jìn)行同步)矾瘾。

使用Lock接口的最常用的實現(xiàn)ReentrantLock(重入鎖)來實現(xiàn)可見性:當(dāng)我們在方法的開始位置執(zhí)行l(wèi)ock.lock()方法女轿,這和synchronized開始位置(Monitor Enter)有相同的語義,即使用共享變量時會從主內(nèi)存中刷新變量值到工作內(nèi)存中(即從主內(nèi)存中讀取最新值到線程私有的工作內(nèi)存中)壕翩,在方法的最后finally塊里執(zhí)行l(wèi)ock.unlock()方法蛉迹,和synchronized結(jié)束位置(Monitor Exit)有相同的語義,即會將工作內(nèi)存中的變量值同步到主內(nèi)存中去(即將線程私有的工作內(nèi)存中的值寫入到主內(nèi)存進(jìn)行同步)。

final關(guān)鍵字的可見性是指:被final修飾的變量放妈,在構(gòu)造函數(shù)數(shù)一旦初始化完成北救,并且在構(gòu)造函數(shù)中并沒有把“this”的引用傳遞出去(“this”引用逃逸是很危險的,其他的線程很可能通過該引用訪問到只“初始化一半”的對象)芜抒,那么其他線程就可以看到final變量的值珍策。

有序性

對于一個線程的代碼而言,我們總是以為代碼的執(zhí)行是從前往后的宅倒,依次執(zhí)行的攘宙。這么說不能說完全不對,在單線程程序里拐迁,確實會這樣執(zhí)行蹭劈;但是在多線程并發(fā)時,程序的執(zhí)行就有可能出現(xiàn)亂序线召。用一句話可以總結(jié)為:在本線程內(nèi)觀察链方,操作都是有序的;如果在一個線程中觀察另外一個線程灶搜,所有的操作都是無序的祟蚀。前半句是指“線程內(nèi)表現(xiàn)為串行語義(WithIn Thread As-if-Serial Semantics)”,后半句是指“指令重排”現(xiàn)象和“工作內(nèi)存和主內(nèi)存同步延遲”現(xiàn)象工窍。

Java提供了兩個關(guān)鍵字volatile和synchronized來保證多線程之間操作的有序性,volatile關(guān)鍵字本身通過加入內(nèi)存屏障來禁止指令的重排序,而synchronized關(guān)鍵字通過一個變量在同一時間只允許有一個線程對其進(jìn)行加鎖的規(guī)則來實現(xiàn)前酿,在單線程程序中患雏,不會發(fā)生“指令重排”和“工作內(nèi)存和主內(nèi)存同步延遲”現(xiàn)象,只在多線程程序中出現(xiàn)罢维。

happens-before原則

Java內(nèi)存模型中定義的兩項操作之間的次序關(guān)系淹仑,如果說操作A先行發(fā)生于操作B,操作A產(chǎn)生的影響能被操作B觀察到肺孵,“影響”包含了修改了內(nèi)存中共享變量的值匀借、發(fā)送了消息、調(diào)用了方法等平窘。

下面是Java內(nèi)存模型下一些”天然的“happens-before關(guān)系吓肋,這些happens-before關(guān)系無須任何同步器協(xié)助就已經(jīng)存在,可以在編碼中直接使用瑰艘。如果兩個操作之間的關(guān)系不在此列是鬼,并且無法從下列規(guī)則推導(dǎo)出來的話,它們就沒有順序性保障紫新,虛擬機(jī)可以對它們進(jìn)行隨意地重排序均蜜。

在這里我推薦下我自己建的Java學(xué)習(xí)群:479121291,歡迎初學(xué)和進(jìn)階中的Java小伙伴進(jìn)入學(xué)習(xí)交流芒率。

程序次序規(guī)則(Pragram Order Rule):在一個線程內(nèi)囤耳,按照程序代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作偶芍。準(zhǔn)確地說應(yīng)該是控制流順序而不是程序代碼順序充择,因為要考慮分支、循環(huán)結(jié)構(gòu)腋寨。

管程鎖定規(guī)則(Monitor Lock Rule):一個unlock操作先行發(fā)生于后面對同一個鎖的lock操作聪铺。這里必須強(qiáng)調(diào)的是同一個鎖,而”后面“是指時間上的先后順序萄窜。

volatile變量規(guī)則(Volatile Variable Rule):對一個volatile變量的寫操作先行發(fā)生于后面對這個變量的讀取操作铃剔,這里的”后面“同樣指時間上的先后順序。

線程啟動規(guī)則(Thread Start Rule):Thread對象的start()方法先行發(fā)生于此線程的每一個動作查刻。

線程終于規(guī)則(Thread Termination Rule):線程中的所有操作都先行發(fā)生于對此線程的終止檢測键兜,我們可以通過Thread.join()方法結(jié)束,Thread.isAlive()的返回值等作段檢測到線程已經(jīng)終止執(zhí)行穗泵。

線程中斷規(guī)則(Thread Interruption Rule):對線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生普气,可以通過Thread.interrupted()方法檢測是否有中斷發(fā)生。

對象終結(jié)規(guī)則(Finalizer Rule):一個對象初始化完成(構(gòu)造方法執(zhí)行完成)先行發(fā)生于它的finalize()方法的開始佃延。

傳遞性(Transitivity):如果操作A先行發(fā)生于操作B现诀,操作B先行發(fā)生于操作C夷磕,那就可以得出操作A先行發(fā)生于操作C的結(jié)論。

一個操作”時間上的先發(fā)生“不代表這個操作會是“先行發(fā)生"仔沿,那如果一個操作"先行發(fā)生"是否就能推導(dǎo)出這個操作必定是"時間上的先發(fā)生"呢坐桩?也是不成立的,一個典型的例子就是指令重排序封锉。所以時間上的先后順序與happens-before原則之間基本沒有什么關(guān)系绵跷,所以衡量并發(fā)安全問題一切必須以happens-before 原則為準(zhǔn)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末成福,一起剝皮案震驚了整個濱河市碾局,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌奴艾,老刑警劉巖净当,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異握侧,居然都是意外死亡蚯瞧,警方通過查閱死者的電腦和手機(jī)嘿期,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進(jìn)店門品擎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人备徐,你說我怎么就攤上這事萄传。” “怎么了蜜猾?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵秀菱,是天一觀的道長。 經(jīng)常有香客問我蹭睡,道長衍菱,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任肩豁,我火速辦了婚禮脊串,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘清钥。我一直安慰自己琼锋,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布祟昭。 她就那樣靜靜地躺著缕坎,像睡著了一般。 火紅的嫁衣襯著肌膚如雪篡悟。 梳的紋絲不亂的頭發(fā)上谜叹,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天匾寝,我揣著相機(jī)與錄音,去河邊找鬼荷腊。 笑死旗吁,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的停局。 我是一名探鬼主播很钓,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼董栽!你這毒婦竟也來了码倦?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤锭碳,失蹤者是張志新(化名)和其女友劉穎袁稽,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體擒抛,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡推汽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了歧沪。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片歹撒。...
    茶點(diǎn)故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖诊胞,靈堂內(nèi)的尸體忽然破棺而出暖夭,到底是詐尸還是另有隱情,我是刑警寧澤撵孤,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布迈着,位于F島的核電站,受9級特大地震影響邪码,放射性物質(zhì)發(fā)生泄漏裕菠。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一闭专、第九天 我趴在偏房一處隱蔽的房頂上張望奴潘。 院中可真熱鬧,春花似錦喻圃、人聲如沸萤彩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽雀扶。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間愚墓,已是汗流浹背予权。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留浪册,地道東北人扫腺。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像村象,于是被迫代替她去往敵國和親笆环。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評論 2 355

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