全面理解Java內(nèi)存模型

Java內(nèi)存模型即Java Memory Model供填,簡稱JMM。JMM定義了Java 虛擬機(jī)(JVM)在計算機(jī)內(nèi)存(RAM)中的工作方式。JVM是整個計算機(jī)虛擬模型,所以JMM是隸屬于JVM的罚拟。

如果我們要想深入了解Java并發(fā)編程,就要先理解好Java內(nèi)存模型完箩。Java內(nèi)存模型定義了多線程之間共享變量的可見性以及如何在需要的時候?qū)蚕碜兞窟M(jìn)行同步赐俗。原始的Java內(nèi)存模型效率并不是很理想,因此Java1.5版本對其進(jìn)行了重構(gòu)弊知,現(xiàn)在的Java8仍沿用了Java1.5的版本阻逮。

關(guān)于并發(fā)編程

在并發(fā)編程領(lǐng)域,有兩個關(guān)鍵問題:線程之間的通信同步秩彤。

線程之間的通信

線程的通信是指線程之間以何種機(jī)制來交換信息叔扼。在命令式編程中,線程之間的通信機(jī)制有兩種共享內(nèi)存消息傳遞漫雷。

共享內(nèi)存的并發(fā)模型里瓜富,線程之間共享程序的公共狀態(tài),線程之間通過寫-讀內(nèi)存中的公共狀態(tài)來隱式進(jìn)行通信珊拼,典型的共享內(nèi)存通信方式就是通過共享對象進(jìn)行通信食呻。

消息傳遞的并發(fā)模型里,線程之間沒有公共狀態(tài)澎现,線程之間必須通過明確的發(fā)送消息來顯式進(jìn)行通信仅胞,在java中典型的消息傳遞方式就是wait()notify()

關(guān)于Java線程之間的通信剑辫,可以參考線程之間的通信(thread signal)干旧。

線程之間的同步

同步是指程序用于控制不同線程之間操作發(fā)生相對順序的機(jī)制。

在共享內(nèi)存并發(fā)模型里妹蔽,同步是顯式進(jìn)行的椎眯。程序員必須顯式指定某個方法或某段代碼需要在線程之間互斥執(zhí)行。

在消息傳遞的并發(fā)模型里胳岂,由于消息的發(fā)送必須在消息的接收之前编整,因此同步是隱式進(jìn)行的。

Java的并發(fā)采用的是共享內(nèi)存模型

Java線程之間的通信總是隱式進(jìn)行乳丰,整個通信過程對程序員完全透明掌测。如果編寫多線程程序的Java程序員不理解隱式進(jìn)行的線程之間通信的工作機(jī)制,很可能會遇到各種奇怪的內(nèi)存可見性問題产园。

Java內(nèi)存模型

上面講到了Java線程之間的通信采用的是過共享內(nèi)存模型汞斧,這里提到的共享內(nèi)存模型指的就是Java內(nèi)存模型(簡稱JMM),JMM決定一個線程對共享變量的寫入何時對另一個線程可見什燕。從抽象的角度來看粘勒,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲在主內(nèi)存(main memory)中,每個線程都有一個私有的本地內(nèi)存(local memory)屎即,本地內(nèi)存中存儲了該線程以讀/寫共享變量的副本庙睡。本地內(nèi)存是JMM的一個抽象概念,并不真實存在技俐。它涵蓋了緩存埃撵,寫緩沖區(qū),寄存器以及其他的硬件和編譯器優(yōu)化虽另。

從上圖來看暂刘,線程A與線程B之間如要通信的話,必須要經(jīng)歷下面2個步驟:

1. 首先捂刺,線程A把本地內(nèi)存A中更新過的共享變量刷新到主內(nèi)存中去谣拣。

2. 然后,線程B到主內(nèi)存中去讀取線程A之前已更新過的共享變量族展。

1

2

3

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

如上圖所示森缠,本地內(nèi)存A和B有主內(nèi)存中共享變量x的副本。假設(shè)初始時仪缸,這三個內(nèi)存中的x值都為0贵涵。線程A在執(zhí)行時,把更新后的x值(假設(shè)值為1)臨時存放在自己的本地內(nèi)存A中。當(dāng)線程A和線程B需要通信時宾茂,線程A首先會把自己本地內(nèi)存中修改后的x值刷新到主內(nèi)存中瓷马,此時主內(nèi)存中的x值變?yōu)榱?。隨后跨晴,線程B到主內(nèi)存中去讀取線程A更新后的x值欧聘,此時線程B的本地內(nèi)存的x值也變?yōu)榱?。

從整體來看端盆,這兩個步驟實質(zhì)上是線程A在向線程B發(fā)送消息怀骤,而且這個通信過程必須要經(jīng)過主內(nèi)存。JMM通過控制主內(nèi)存與每個線程的本地內(nèi)存之間的交互焕妙,來為java程序員提供內(nèi)存可見性保證蒋伦。

上面也說到了,Java內(nèi)存模型只是一個抽象概念焚鹊,那么它在Java中具體是怎么工作的呢痕届?為了更好的理解上Java內(nèi)存模型工作方式,下面就JVM對Java內(nèi)存模型的實現(xiàn)寺旺、硬件內(nèi)存模型及它們之間的橋接做詳細(xì)介紹爷抓。

JVM對Java內(nèi)存模型的實現(xiàn)

在JVM內(nèi)部,Java內(nèi)存模型把內(nèi)存分成了兩部分:線程棧區(qū)和堆區(qū)阻塑,下圖展示了Java內(nèi)存模型在JVM中的邏輯視圖:?


JVM中運行的每個線程都擁有自己的線程棧蓝撇,線程棧包含了當(dāng)前線程執(zhí)行的方法調(diào)用相關(guān)信息,我們也把它稱作調(diào)用棧陈莽。隨著代碼的不斷執(zhí)行渤昌,調(diào)用棧會不斷變化。

線程棧還包含了當(dāng)前方法的所有本地變量信息走搁。一個線程只能讀取自己的線程棧独柑,也就是說,線程中的本地變量對其它線程是不可見的私植。即使兩個線程執(zhí)行的是同一段代碼忌栅,它們也會各自在自己的線程棧中創(chuàng)建本地變量,因此曲稼,每個線程中的本地變量都會有自己的版本索绪。

所有原始類型(boolean,byte,short,char,int,long,float,double)的本地變量都直接保存在線程棧當(dāng)中,對于它們的值各個線程之間都是獨立的贫悄。對于原始類型的本地變量瑞驱,一個線程可以傳遞一個副本給另一個線程,當(dāng)它們之間是無法共享的窄坦。

堆區(qū)包含了Java應(yīng)用創(chuàng)建的所有對象信息唤反,不管對象是哪個線程創(chuàng)建的凳寺,其中的對象包括原始類型的封裝類(如Byte、Integer彤侍、Long等等)肠缨。不管對象是屬于一個成員變量還是方法中的本地變量,它都會被存儲在堆區(qū)拥刻。

下圖展示了調(diào)用棧和本地變量都存儲在棧區(qū)怜瞒,對象都存儲在堆區(qū):?


一個本地變量如果是原始類型父泳,那么它會被完全存儲到棧區(qū)般哼。?

一個本地變量也有可能是一個對象的引用,這種情況下惠窄,這個本地引用會被存儲到棧中蒸眠,但是對象本身仍然存儲在堆區(qū)。

對于一個對象的成員方法杆融,這些方法中包含本地變量楞卡,仍需要存儲在棧區(qū),即使它們所屬的對象在堆區(qū)脾歇。?

對于一個對象的成員變量蒋腮,不管它是原始類型還是包裝類型,都會被存儲到堆區(qū)藕各。

Static類型的變量以及類本身相關(guān)信息都會隨著類本身存儲在堆區(qū)池摧。

堆中的對象可以被多線程共享。如果一個線程獲得一個對象的應(yīng)用激况,它便可訪問這個對象的成員變量作彤。如果兩個線程同時調(diào)用了同一個對象的同一個方法,那么這兩個線程便可同時訪問這個對象的成員變量乌逐,但是對于本地變量竭讳,每個線程都會拷貝一份到自己的線程棧中。

下圖展示了上面描述的過程:?

硬件內(nèi)存架構(gòu)

不管是什么內(nèi)存模型浙踢,最終還是運行在計算機(jī)硬件上的绢慢,所以我們有必要了解計算機(jī)硬件內(nèi)存架構(gòu),下圖就簡單描述了當(dāng)代計算機(jī)硬件內(nèi)存架構(gòu):?

現(xiàn)代計算機(jī)一般都有2個以上CPU洛波,而且每個CPU還有可能包含多個核心胰舆。因此,如果我們的應(yīng)用是多線程的話奋岁,這些線程可能會在各個CPU核心中并行運行思瘟。

在CPU內(nèi)部有一組CPU寄存器,也就是CPU的儲存器闻伶。CPU操作寄存器的速度要比操作計算機(jī)主存快的多滨攻。在主存和CPU寄存器之間還存在一個CPU緩存,CPU操作CPU緩存的速度快于主存但慢于CPU寄存器。某些CPU可能有多個緩存層(一級緩存和二級緩存)光绕。計算機(jī)的主存也稱作RAM女嘲,所有的CPU都能夠訪問主存,而且主存比上面提到的緩存和寄存器大很多诞帐。

當(dāng)一個CPU需要訪問主存時欣尼,會先讀取一部分主存數(shù)據(jù)到CPU緩存,進(jìn)而在讀取CPU緩存到寄存器停蕉。當(dāng)CPU需要寫數(shù)據(jù)到主存時愕鼓,同樣會先flush寄存器到CPU緩存,然后再在某些節(jié)點把緩存數(shù)據(jù)flush到主存慧起。

Java內(nèi)存模型和硬件架構(gòu)之間的橋接

正如上面講到的菇晃,Java內(nèi)存模型和硬件內(nèi)存架構(gòu)并不一致。硬件內(nèi)存架構(gòu)中并沒有區(qū)分棧和堆蚓挤,從硬件上看磺送,不管是棧還是堆,大部分?jǐn)?shù)據(jù)都會存到主存中灿意,當(dāng)然一部分棧和堆的數(shù)據(jù)也有可能會存到CPU寄存器中估灿,如下圖所示,Java內(nèi)存模型和計算機(jī)硬件內(nèi)存架構(gòu)是一個交叉關(guān)系:?

當(dāng)對象和變量存儲到計算機(jī)的各個內(nèi)存區(qū)域時缤剧,必然會面臨一些問題馅袁,其中最主要的兩個問題是:

1. 共享對象對各個線程的可見性

2. 共享對象的競爭現(xiàn)象

1

2

3

共享對象的可見性

當(dāng)多個線程同時操作同一個共享對象時,如果沒有合理的使用volatile和synchronization關(guān)鍵字鞭执,一個線程對共享對象的更新有可能導(dǎo)致其它線程不可見司顿。

想象一下我們的共享對象存儲在主存,一個CPU中的線程讀取主存數(shù)據(jù)到CPU緩存兄纺,然后對共享對象做了更改大溜,但CPU緩存中的更改后的對象還沒有flush到主存,此時線程對共享對象的更改對其它CPU中的線程是不可見的估脆。最終就是每個線程最終都會拷貝共享對象钦奋,而且拷貝的對象位于不同的CPU緩存中。

下圖展示了上面描述的過程疙赠。左邊CPU中運行的線程從主存中拷貝共享對象obj到它的CPU緩存付材,把對象obj的count變量改為2。但這個變更對運行在右邊CPU中的線程不可見圃阳,因為這個更改還沒有flush到主存中:?


要解決共享對象可見性這個問題厌衔,我們可以使用java volatile關(guān)鍵字。 Java’s volatile keyword. volatile 關(guān)鍵字可以保證變量會直接從主存讀取捍岳,而對變量的更新也會直接寫到主存富寿。volatile原理是基于CPU內(nèi)存屏障指令實現(xiàn)的睬隶,后面會講到。

競爭現(xiàn)象

如果多個線程共享一個對象页徐,如果它們同時修改這個共享對象苏潜,這就產(chǎn)生了競爭現(xiàn)象。

如下圖所示变勇,線程A和線程B共享一個對象obj恤左。假設(shè)線程A從主存讀取Obj.count變量到自己的CPU緩存,同時搀绣,線程B也讀取了Obj.count變量到它的CPU緩存飞袋,并且這兩個線程都對Obj.count做了加1操作。此時豌熄,Obj.count加1操作被執(zhí)行了兩次授嘀,不過都在不同的CPU緩存中物咳。

如果這兩個加1操作是串行執(zhí)行的锣险,那么Obj.count變量便會在原始值上加2,最終主存中的Obj.count的值會是3览闰。然而下圖中兩個加1操作是并行的芯肤,不管是線程A還是線程B先flush計算結(jié)果到主存,最終主存中的Obj.count只會增加1次變成2压鉴,盡管一共有兩次加1操作崖咨。?

要解決上面的問題我們可以使用java synchronized代碼塊。synchronized代碼塊可以保證同一個時刻只能有一個線程進(jìn)入代碼競爭區(qū)油吭,synchronized代碼塊也能保證代碼塊中所有變量都將會從主存中讀击蹲,當(dāng)線程退出代碼塊時,對所有變量的更新將會flush到主存婉宰,不管這些變量是不是volatile類型的歌豺。

volatile和 synchronized區(qū)別

詳細(xì)請見?volatile和synchronized的區(qū)別

支撐Java內(nèi)存模型的基礎(chǔ)原理

指令重排序

在執(zhí)行程序時,為了提高性能心包,編譯器和處理器會對指令做重排序类咧。但是,JMM確保在不同的編譯器和不同的處理器平臺之上蟹腾,通過插入特定類型的Memory Barrier來禁止特定類型的編譯器重排序和處理器重排序痕惋,為上層提供一致的內(nèi)存可見性保證。

編譯器優(yōu)化重排序:編譯器在不改變單線程程序語義的前提下娃殖,可以重新安排語句的執(zhí)行順序值戳。

指令級并行的重排序:如果不存l在數(shù)據(jù)依賴性,處理器可以改變語句對應(yīng)機(jī)器指令的執(zhí)行順序炉爆。

內(nèi)存系統(tǒng)的重排序:處理器使用緩存和讀寫緩沖區(qū)堕虹,這使得加載和存儲操作看上去可能是在亂序執(zhí)行柿隙。

數(shù)據(jù)依賴性

如果兩個操作訪問同一個變量,其中一個為寫操作鲫凶,此時這兩個操作之間存在數(shù)據(jù)依賴性禀崖。?

編譯器和處理器不會改變存在數(shù)據(jù)依賴性關(guān)系的兩個操作的執(zhí)行順序,即不會重排序螟炫。

as-if-serial

不管怎么重排序波附,單線程下的執(zhí)行結(jié)果不能被改變,編譯器昼钻、runtime和處理器都必須遵守as-if-serial語義掸屡。

內(nèi)存屏障(Memory Barrier )

上面講到了,通過內(nèi)存屏障可以禁止特定類型處理器的重排序然评,從而讓程序按我們預(yù)想的流程去執(zhí)行仅财。內(nèi)存屏障,又稱內(nèi)存柵欄碗淌,是一個CPU指令盏求,基本上它是一條這樣的指令:

保證特定操作的執(zhí)行順序。

影響某些數(shù)據(jù)(或則是某條指令的執(zhí)行結(jié)果)的內(nèi)存可見性亿眠。

編譯器和CPU能夠重排序指令碎罚,保證最終相同的結(jié)果,嘗試優(yōu)化性能纳像。插入一條Memory Barrier會告訴編譯器和CPU:不管什么指令都不能和這條Memory Barrier指令重排序荆烈。

Memory Barrier所做的另外一件事是強制刷出各種CPU cache,如一個Write-Barrier(寫入屏障)將刷出所有在Barrier之前寫入 cache 的數(shù)據(jù)竟趾,因此憔购,任何CPU上的線程都能讀取到這些數(shù)據(jù)的最新版本。

這和java有什么關(guān)系岔帽?上面java內(nèi)存模型中講到的volatile是基于Memory Barrier實現(xiàn)的玫鸟。

如果一個變量是volatile修飾的,JMM會在寫入這個字段之后插進(jìn)一個Write-Barrier指令山卦,并在讀這個字段之前插入一個Read-Barrier指令鞋邑。這意味著,如果寫入一個volatile變量账蓉,就可以保證:

一個線程寫入變量a后枚碗,任何線程訪問該變量都會拿到最新值。

在寫入變量a之前的寫入操作铸本,其更新的數(shù)據(jù)對于其他線程也是可見的肮雨。因為Memory Barrier會刷出cache中的所有先前的寫入。

happens-before

從jdk5開始箱玷,java使用新的JSR-133內(nèi)存模型怨规,基于happens-before的概念來闡述操作之間的內(nèi)存可見性陌宿。

在JMM中,如果一個操作的執(zhí)行結(jié)果需要對另一個操作可見波丰,那么這兩個操作之間必須要存在happens-before關(guān)系壳坪,這個的兩個操作既可以在同一個線程,也可以在不同的兩個線程中掰烟。

與程序員密切相關(guān)的happens-before規(guī)則如下:

程序順序規(guī)則:一個線程中的每個操作爽蝴,happens-before于該線程中任意的后續(xù)操作。

監(jiān)視器鎖規(guī)則:對一個鎖的解鎖操作纫骑,happens-before于隨后對這個鎖的加鎖操作蝎亚。

volatile域規(guī)則:對一個volatile域的寫操作,happens-before于任意線程后續(xù)對這個volatile域的讀先馆。

傳遞性規(guī)則:如果 A happens-before B发框,且 B happens-before C,那么A happens-before C煤墙。

注意:兩個操作之間具有happens-before關(guān)系梅惯,并不意味前一個操作必須要在后一個操作之前執(zhí)行!僅僅要求前一個操作的執(zhí)行結(jié)果番捂,對于后一個操作是可見的个唧,且前一個操作按順序排在后一個操作之前。

參考文檔 :?

1.?http://www.infoq.com/cn/articles/java-memory-model-1?

2.?http://www.reibang.com/p/d3fda02d4cae

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末设预,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子犁河,更是在濱河造成了極大的恐慌鳖枕,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件桨螺,死亡現(xiàn)場離奇詭異宾符,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)灭翔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進(jìn)店門魏烫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人肝箱,你說我怎么就攤上這事哄褒。” “怎么了煌张?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵呐赡,是天一觀的道長。 經(jīng)常有香客問我骏融,道長链嘀,這世上最難降的妖魔是什么萌狂? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮怀泊,結(jié)果婚禮上茫藏,老公的妹妹穿的比我還像新娘。我一直安慰自己霹琼,他們只是感情好刷允,可當(dāng)我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著碧囊,像睡著了一般树灶。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上糯而,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天天通,我揣著相機(jī)與錄音,去河邊找鬼熄驼。 笑死像寒,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的瓜贾。 我是一名探鬼主播诺祸,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼祭芦!你這毒婦竟也來了筷笨?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤龟劲,失蹤者是張志新(化名)和其女友劉穎胃夏,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體昌跌,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡仰禀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蚕愤。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片答恶。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖萍诱,靈堂內(nèi)的尸體忽然破棺而出悬嗓,到底是詐尸還是另有隱情,我是刑警寧澤砂沛,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布烫扼,位于F島的核電站,受9級特大地震影響碍庵,放射性物質(zhì)發(fā)生泄漏映企。R本人自食惡果不足惜悟狱,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望堰氓。 院中可真熱鬧挤渐,春花似錦、人聲如沸双絮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽囤攀。三九已至软免,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間焚挠,已是汗流浹背膏萧。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留蝌衔,地道東北人榛泛。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像噩斟,于是被迫代替她去往敵國和親曹锨。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,060評論 2 355

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