JVM(七)JMM內(nèi)存模型

1.JMM產(chǎn)生背景和定義

JMM(Java內(nèi)存模型)源于物理機(jī)CPU架構(gòu)的內(nèi)存模型,最初用于解決MP(多處理器架構(gòu))系統(tǒng)中的緩存一致性問題,而JVM為了屏蔽與各個(gè)硬件平臺(tái)和操作系統(tǒng)對(duì)內(nèi)存訪問機(jī)制的差異化,提出了JMM的概念。Java內(nèi)存模型是一種虛擬機(jī)規(guī)范,JMM規(guī)范了Java虛擬機(jī)與計(jì)算機(jī)內(nèi)存時(shí)如何協(xié)同工作的:規(guī)定了一個(gè)線程如何和何時(shí)可以看到由其他線程修改過后的共享變量的值却特,以及在必須時(shí)如何同步的訪問共享變量。

2.Java內(nèi)存模型和操作系統(tǒng)內(nèi)存模型的關(guān)系

內(nèi)存模型和操作系統(tǒng)內(nèi)存模型的關(guān)系

Java內(nèi)存模型的主要目標(biāo)是定義程序中各個(gè)變量的訪問規(guī)則筛圆,此處提到的變量只包含實(shí)例對(duì)象裂明、靜態(tài)對(duì)象和構(gòu)成數(shù)組對(duì)象的元素(堆和方法區(qū)中共享的部分),局部變量和方法參數(shù)是線程私有的太援,不會(huì)共享闽晦。當(dāng)然不會(huì)存在數(shù)據(jù)競(jìng)爭(zhēng)問題。

JMM規(guī)定了所有變量存儲(chǔ)在主內(nèi)存(Main Memory)中提岔。每個(gè)線程還有自己的工作內(nèi)存(Working Memory)仙蛉,線程的工作內(nèi)存中保存了該線程使用到的變量的主內(nèi)存的副本拷貝,線程對(duì)變量的所有操作(讀取碱蒙、賦值等)都必須在工作內(nèi)存進(jìn)行荠瘪,而不能直接讀寫主內(nèi)存中的變量(volatile變量仍然有工作內(nèi)存的拷貝,由于它特殊性的操作順序規(guī)定赛惩,所以看起來如同直接在主內(nèi)存中讀寫訪問一般)哀墓。不同線程之間也無法直接訪問對(duì)方工作內(nèi)存中的變量,線程之間值的傳遞都需要通過主內(nèi)存來完成喷兼。

對(duì)于JMM與JVM本身的內(nèi)存模型篮绰,參照《深入理解Java虛擬機(jī)》的解釋,主內(nèi)存季惯、工作內(nèi)存與Java內(nèi)存區(qū)域中的Java堆吠各、棧、方法區(qū)等并不是同一個(gè)層次的內(nèi)存劃分星瘾。如果兩者一定要勉強(qiáng)對(duì)應(yīng)起來走孽,那從變量、主內(nèi)存琳状、工作內(nèi)存的定義來看,主內(nèi)存對(duì)應(yīng)于Java堆中對(duì)象的實(shí)例數(shù)據(jù)部分盒齿,而工作內(nèi)存則對(duì)應(yīng)虛擬機(jī)中的局部變量表念逞,寄存器對(duì)應(yīng)著操作數(shù)棧困食。

3.Java內(nèi)存的抽象結(jié)構(gòu)

Java 線程之間的通信由JMM控制,JMM決定一個(gè)線程對(duì)共享變量寫入何時(shí)對(duì)另一個(gè)線程可見翎承。從抽象的角度來看硕盹,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲(chǔ)在主內(nèi)存(Main Memory)中,每個(gè)線程都有一個(gè)私有的本地內(nèi)存(Local Memory)叨咖,本地內(nèi)存中存儲(chǔ)了該線程共享變量的副本瘩例。本地內(nèi)存是一個(gè)抽象概念,它涵蓋了緩存甸各、寫緩沖區(qū)垛贤、寄存器以及其他的硬件和編譯器優(yōu)化。Java內(nèi)存模型的抽象示意如圖所示:

內(nèi)存模型示意圖

線程A與線程B之間要通信的話趣倾,必須要經(jīng)歷兩個(gè)步驟聘惦。

  • 1.線程A把本地內(nèi)存中更新過的共享變量刷新到主內(nèi)存中去。
  • 2.線程B到主內(nèi)存中區(qū)讀取線程A之前已經(jīng)更新過的共享變量儒恋。

4.指令重排

計(jì)算機(jī)在執(zhí)行程序時(shí)善绎,為了提高性能,編譯器和處理器常常會(huì)對(duì)指令做重排诫尽,一般分為以下3種

  • 編譯器優(yōu)化的重排:編譯器在不改變單線程程序語義的前提下禀酱,可以重新安排語句的執(zhí)行順序。
  • 指令并行的重排:處理器采用指令級(jí)并行技術(shù)來將多條指令并行執(zhí)行牧嫉,如果不存在后一個(gè)執(zhí)行語句依賴前面執(zhí)行語句的情況比勉,處理器可以改變語句對(duì)應(yīng)的機(jī)器指令的執(zhí)行順序。
  • 內(nèi)存系統(tǒng)的重排:由于處理器使用緩存和讀寫緩沖區(qū)驹止,這使得加載和存儲(chǔ)操作看上去是亂序執(zhí)行的浩聋。因?yàn)槿?jí)緩存的存在,導(dǎo)致內(nèi)存與緩存的數(shù)據(jù)同步存在時(shí)間差臊恋。

其中編譯器的重排優(yōu)化屬于編譯期重排衣洁,指令并行的重排和內(nèi)存系統(tǒng)的重排屬于處理器重排,這些重排優(yōu)化可能會(huì)導(dǎo)致程序出現(xiàn)內(nèi)存可見性問題抖仅。

5.JMM內(nèi)存模型作用

JMM是圍繞著程序執(zhí)行的原子性坊夫、有序性、可見性展開的撤卢,它是如何保證這三種特性的呢环凿?
我們先了解下這三種特性。

1.原子性

在Java中放吩,java內(nèi)存模型只保證對(duì)基本數(shù)據(jù)類型變量的讀取和賦值操作是原子性操作(long智听、double)除外,即這些操作是不可被中斷的,要么全部執(zhí)行到推,要么不執(zhí)行考赛。例如下面代碼

x = 10;   
y = x;
x++;
x = x + 1;

只有x= 10;是原子性操作,其他三個(gè)語句都不是原子性操作莉测。

x = 10;是直接將數(shù)值10賦給x颜骤,也就是說線程執(zhí)行這個(gè)語句會(huì)直接將數(shù)值10寫入到工作內(nèi)存中。
y = x ;包含兩個(gè)操作捣卤,它先要去讀取x的值忍抽,再將x的值賦值給y寫入工作內(nèi)存。雖然讀取x的值以及將y的值寫入工作內(nèi)存這兩個(gè)操作都是原子性操作董朝,但是合起來就不是原子性操作了鸠项。
同樣的 x++和x = x+1;包括三個(gè)操作,讀取x的值益涧,進(jìn)行加1操作锈锤,寫入新的值。

所以闲询,上面4個(gè)語句中有x = 10久免;的操作具備原子性。也就是說扭弧,只有簡(jiǎn)單的讀取阎姥、賦值才是原子操作。(而且必須是將基礎(chǔ)類型常量賦值給某個(gè)變量鸽捻,變量之間的相互賦值不是原子操作)

2.可見性

每個(gè)線程會(huì)在自己的工作內(nèi)存來操作共享變量呼巴,操作完成之后將工作內(nèi)存中的副本寫回主內(nèi)存,并且在其他線程從主內(nèi)存將變量同步回自己的工作內(nèi)存之前御蒲,共享變量的改變對(duì)其他線程是不可見的衣赶。

當(dāng)一個(gè)線程修改了共享變量的值,其他線程能夠立即得知這個(gè)修改后的值厚满,JMM通過在變量修改后將新值同步回主內(nèi)存府瞄,其他線程在變量讀取前從主內(nèi)存讀取新值刷新到工作內(nèi)存。這種依賴主內(nèi)存作為傳遞媒介的方法來實(shí)現(xiàn)可見性碘箍。

volatile之可見性

當(dāng)一個(gè)共享變量被volatile修飾時(shí)遵馆,它會(huì)保證當(dāng)前線程修改的值立即被更新到主存。
其他線程工作內(nèi)存中的變量會(huì)強(qiáng)制立即失效丰榴,當(dāng)其他線程需要讀取時(shí)货邓,回去主內(nèi)存中讀取最新值。

而普通的共享變量不能保證可見性四濒,因?yàn)槠胀ü蚕碜兞勘恍薷暮蠡豢觯裁磿r(shí)候被寫入主存是不確定的职辨,當(dāng)其他線程去讀取時(shí),此時(shí)內(nèi)存中可能還是原來的舊值复隆,因此無法保證可見性拨匆。

3.有序性

在Java內(nèi)存模型中姆涩,允許編譯器和處理器對(duì)指令進(jìn)行重排序挽拂,重排序過程不會(huì)影響到單線程的執(zhí)行,卻會(huì)影響到多線程并發(fā)執(zhí)行的正確性骨饿。

例子:指令重排導(dǎo)致DCL失效

public class Singleton {
     // 指向自己實(shí)例的私有靜態(tài)引用
    private static Singleton instance = null;
    // 私有的構(gòu)造方法
    private Singleton() { }
    // 以自己實(shí)例為返回值的靜態(tài)的公有方法亏栈,靜態(tài)工廠方法
    public static Singleton getInstance() {
            // 被動(dòng)創(chuàng)建,在真正需要使用時(shí)才去創(chuàng)建
            if(instance == null) {
                //同一時(shí)刻只有一個(gè)線程進(jìn)入同步塊執(zhí)行創(chuàng)建對(duì)象
                synchronzied(Singleton.class) {
                    if(instance == null) {
                        instance = new Singleton();
                    }
                }
            }
        return instance;
    }
}

看似簡(jiǎn)單的一行賦值語句:instance = new Singleton();其實(shí)內(nèi)部已經(jīng)轉(zhuǎn)為多條指令宏赘。

memory = allocate();    //分配內(nèi)存
ctorInstance(memory); //初始化對(duì)象
instance = memory;     //設(shè)置instance引用指向剛才分配的內(nèi)存地址

但是有可能經(jīng)過指令重排后如下

memory = allocate();    //分配內(nèi)存
instance = memory;     //設(shè)置instance引用指向剛才分配的內(nèi)存地址
ctorInstance(memory); //初始化對(duì)象

可以看到指令重排后绒北,instance指向分配好的內(nèi)存放在了前面,在線程A初始化完成這段內(nèi)存之前察署,線程B雖然進(jìn)不去同步代碼塊闷游,但是在同步代碼塊之前的判斷就會(huì)發(fā)現(xiàn)instance不為空,此時(shí)線程B獲得一個(gè)不完整(未初始化)的 Singleton 對(duì)象進(jìn)行使用就可能發(fā)生錯(cuò)誤贴汪。

volatile之有序性

原理:volatile的可見性和有序性都是通過加入內(nèi)存屏障來實(shí)現(xiàn)脐往。
會(huì)在寫之后加入一條store屏障指令,將本地內(nèi)存中值刷新到主內(nèi)存扳埂。
會(huì)在讀之前加入一條load屏障指令业簿,從主內(nèi)存中讀取共享變量。

synchronized 之有序性
  • JMM中有一條關(guān)于synchronized的規(guī)則:一個(gè)變量在同一時(shí)刻只允許一條線程對(duì)其進(jìn)行l(wèi)ock阳懂。這條規(guī)則決定了同步塊中的操作相當(dāng)于單線程執(zhí)行梅尤。
  • as if serial :不管怎么重排序,單線程的執(zhí)行結(jié)果不能被改變岩调。編譯器和處理器都必須遵守as if serial語義巷燥。也就是說上面的重排序不會(huì)影響到單線程程序的執(zhí)行,所以堆外就表現(xiàn)出了有序性号枕。
4.JMM提供的解決方案

在理解了原子性缰揪、可見性、順序性后堕澄,我們看看JMM如何保證這三種特性邀跃。

  • 1.除了JVM自身提供的對(duì)基本數(shù)據(jù)類型讀寫操作的原子性外,對(duì)于方法級(jí)別或者代碼塊級(jí)別的原子性操作蛙紫,可以使用synchronized 或者lock接口拍屑,保證程序執(zhí)行的原子性。
  • 2.而工作內(nèi)存與主內(nèi)存同步延遲導(dǎo)致的可見性問題坑傅,可以使用synchronized 或者volitile 關(guān)鍵字解決僵驰,他們都可以使一個(gè)線程修改后的變量對(duì)其他線程立即可見。
  • 3.對(duì)于指令重排導(dǎo)致的有序性問題,可以使用volitile 或者synchronized關(guān)鍵字解決蒜茴,volatile另外一個(gè)作用是禁止指令重排星爪,synchronized會(huì)變成單線程操作 as if serial保證了結(jié)果的一致性。
  • 4.除了靠synchronized 和volatile 關(guān)鍵字來保證有序性粉私、原子性顽腾、可見性外,JMM內(nèi)部還定義了一套happens-before原則來保證多線程環(huán)境下兩個(gè)操作的原子性诺核、可見性抄肖、順序性。

6.happens-before

倘若在開發(fā)中窖杀,僅靠synchronized和volatile來保證順序性漓摩、原子性、可見性入客。那么編寫并發(fā)程序會(huì)十分麻煩管毙。在Java內(nèi)存模型中,還提供了happens-before原則來輔助保證程序執(zhí)行的原子性桌硫、可見性夭咬、有序性的問題。它是判斷數(shù)據(jù)是否存在競(jìng)爭(zhēng)鞍泉,線程是否安全的依據(jù)皱埠。happens-before原則內(nèi)容如下:

  • 程序順序原則,即在一個(gè)線程內(nèi)必須保證語義串行性咖驮,也就是說按照代碼順序執(zhí)行边器。
  • 鎖規(guī)則 解鎖(unlock)操作必然發(fā)生在后續(xù)的同一個(gè)鎖的加鎖(lock)之前,也就是說托修,如果對(duì)于一個(gè)鎖解鎖后忘巧,再加鎖,那么加鎖的動(dòng)作必須在解鎖動(dòng)作之后(同一個(gè)鎖)睦刃。
  • volatile規(guī)則 volatile變量的寫砚嘴,先發(fā)生于讀,這保證了volatile變量的可見性涩拙,簡(jiǎn)單的理解就是际长,volatile變量在每次被線程訪問時(shí),都強(qiáng)迫從主內(nèi)存中讀該變量的值兴泥,而當(dāng)該變量發(fā)生變化時(shí)工育,又會(huì)強(qiáng)迫將最新的值刷新到主內(nèi)存,任何時(shí)刻搓彻,不同的線程總是能夠看到該變量的最新值如绸。
  • 線程啟動(dòng)規(guī)則 線程的start()方法先于它的每一個(gè)動(dòng)作嘱朽,即如果線程A在執(zhí)行線程B的start方法之前修改了共享變量的值,那么當(dāng)線程B執(zhí)行start方法時(shí)怔接,線程A對(duì)共享變量的修改對(duì)線程B可見
  • 傳遞性 A先于B 搪泳,B先于C 那么A必然先于C
  • 線程終止規(guī)則 線程的所有操作先于線程的終結(jié),Thread.join()方法的作用是等待當(dāng)前執(zhí)行的線程終止扼脐。假設(shè)在線程B終止之前岸军,修改了共享變量,線程A從線程B的join方法成功返回后谎势,線程B對(duì)共享變量的修改將對(duì)線程A可見凛膏。
  • 線程中斷規(guī)則 對(duì)線程 interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生杨名,可以通過Thread.interrupted()方法檢測(cè)線程是否中斷脏榆。
  • 對(duì)象終結(jié)規(guī)則 對(duì)象的構(gòu)造函數(shù)執(zhí)行,結(jié)束先于finalize()方法

上述8條原則無需手動(dòng)添加任何同步手段(synchronized|volatile)即可達(dá)到效果台谍。

7.volatile 內(nèi)存語義

  • 保證被volatile修飾的共享變量對(duì)所有線程總是可見的须喂。也就是當(dāng)一個(gè)線程修改了一個(gè)被volatile修飾的共享變量的值,新值總是可以被其他線程立即得知趁蕊。
  • 禁止指令重排優(yōu)化坞生。
1.可見性

當(dāng)寫一個(gè)volatile變量時(shí),JMM會(huì)把該線程對(duì)應(yīng)的工作內(nèi)存中的共享變量值刷新到主內(nèi)存中
當(dāng)讀取一個(gè)volatile變量掷伙,JMM會(huì)把該線程對(duì)應(yīng)的工作內(nèi)存置為無效是己,那么該線程將只能從主內(nèi)存中重新讀取共享變量。volatile變量正是通過這種寫-讀的方式實(shí)現(xiàn)對(duì)其他線程可見任柜。(內(nèi)存語義實(shí)現(xiàn)是通過內(nèi)存屏障)

2.禁止重排優(yōu)化

volatile關(guān)鍵字另一個(gè)作用就是禁止指令重排優(yōu)化卒废,從而避免多線程環(huán)境下程序出現(xiàn)亂序的執(zhí)行現(xiàn)象。
內(nèi)存屏障(Memory Barrier)又稱內(nèi)存柵欄宙地,是一個(gè)CPU命令摔认,它的作用有兩個(gè):

  • 1.保證特定操作的執(zhí)行順序。
  • 2.保證某些變量的內(nèi)存可見性宅粥。

由于編譯器和處理器都能執(zhí)行指令重排優(yōu)化参袱,如果在指令間插入一條Memory Barrier則會(huì)告訴編譯器和CPU,不管什么指令都不能和這條內(nèi)存屏障指令重排序秽梅,Memory Barrier的另外一個(gè)作用是強(qiáng)制刷出各種CPU的緩存數(shù)據(jù)抹蚀。
總之,volatile變量正是通過內(nèi)存屏障實(shí)現(xiàn)其在內(nèi)存中的語義企垦,即可見性和禁止重排優(yōu)化环壤。


7.總結(jié)

JMM就是一組規(guī)則,這組規(guī)則解決并發(fā)編程可能出現(xiàn)的線程安全問題竹观。并提供了內(nèi)置解決方案(happens-before原則)及外部可使用的同步手段(synchronized和volatile)镐捧,確保程序在多線程環(huán)境下的原子性潜索、可見性、順序性懂酱。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末竹习,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子列牺,更是在濱河造成了極大的恐慌整陌,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,635評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瞎领,死亡現(xiàn)場(chǎng)離奇詭異泌辫,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)九默,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門震放,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人驼修,你說我怎么就攤上這事殿遂。” “怎么了乙各?”我有些...
    開封第一講書人閱讀 168,083評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵墨礁,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我耳峦,道長(zhǎng)恩静,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,640評(píng)論 1 296
  • 正文 為了忘掉前任蹲坷,我火速辦了婚禮驶乾,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘冠句。我一直安慰自己轻掩,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評(píng)論 6 397
  • 文/花漫 我一把揭開白布懦底。 她就那樣靜靜地躺著唇牧,像睡著了一般。 火紅的嫁衣襯著肌膚如雪聚唐。 梳的紋絲不亂的頭發(fā)上丐重,一...
    開封第一講書人閱讀 52,262評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音杆查,去河邊找鬼扮惦。 笑死,一個(gè)胖子當(dāng)著我的面吹牛亲桦,可吹牛的內(nèi)容都是我干的崖蜜。 我是一名探鬼主播浊仆,決...
    沈念sama閱讀 40,833評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼豫领!你這毒婦竟也來了抡柿?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,736評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤等恐,失蹤者是張志新(化名)和其女友劉穎洲劣,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體课蔬,經(jīng)...
    沈念sama閱讀 46,280評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡囱稽,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了二跋。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片战惊。...
    茶點(diǎn)故事閱讀 40,503評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖同欠,靈堂內(nèi)的尸體忽然破棺而出样傍,到底是詐尸還是另有隱情,我是刑警寧澤铺遂,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站茎刚,受9級(jí)特大地震影響襟锐,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜膛锭,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評(píng)論 3 333
  • 文/蒙蒙 一粮坞、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧初狰,春花似錦莫杈、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至腥光,卻和暖如春关顷,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背武福。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工议双, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人捉片。 一個(gè)月前我還...
    沈念sama閱讀 48,909評(píng)論 3 376
  • 正文 我出身青樓平痰,卻偏偏與公主長(zhǎng)得像汞舱,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子宗雇,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評(píng)論 2 359