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)系
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)存模型的抽象示意如圖所示:
線程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)境下的原子性潜索、可見性、順序性懂酱。