學(xué)號(hào):20021211189? ? ? ?姓名:趙治偉
【嵌牛導(dǎo)讀】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í)如何同步的訪問共享變量老玛。
【嵌牛鼻子】JMM內(nèi)存模型
【嵌牛正文】
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ù)棧慎玖。
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)存可見性問題评疗。
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搀军;的操作具備原子性症虑。也就是說缩歪,只有簡單的讀取、賦值才是原子操作谍憔。(而且必須是將基礎(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失效
publicclassSingleton{// 指向自己實(shí)例的私有靜態(tài)引用privatestaticSingletoninstance=null;// 私有的構(gòu)造方法privateSingleton(){}// 以自己實(shí)例為返回值的靜態(tài)的公有方法,靜態(tài)工廠方法publicstaticSingletongetInstance(){// 被動(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=newSingleton();}}}returninstance;}}
看似簡單的一行賦值語句: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)出了有序性邪乍。
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è)操作的原子性辆苔、可見性、順序性扼劈。
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變量的可見性晓殊,簡單的理解就是断凶,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á)到效果鸣奔。
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)化炬丸。
總結(jié)
JMM就是一組規(guī)則,這組規(guī)則解決并發(fā)編程可能出現(xiàn)的線程安全問題蜒蕾。并提供了內(nèi)置解決方案(happens-before原則)及外部可使用的同步手段(synchronized和volatile)稠炬,確保程序在多線程環(huán)境下的原子性、可見性咪啡、順序性首启。
作者:hadoop_a9bb
鏈接:http://www.reibang.com/p/e034762c85cb
來源:簡書
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán)撤摸,非商業(yè)轉(zhuǎn)載請(qǐng)注明出處毅桃。