Java Memory Model,通過定義程序中各個變量的訪問規(guī)則帆调,即在虛擬機(jī)中將變量存儲到內(nèi)存和從內(nèi)存中取出變量這樣的底層細(xì)節(jié)砚殿,屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問差異。
1.JMM的抽象結(jié)構(gòu)
如下圖所示先朦,線程之間的共享變量(實例域,靜態(tài)域和數(shù)組元素)存儲在主內(nèi)存(Main Memory)中犬缨,每個線程都有一個私有的本地內(nèi)存(Local Memory),本地內(nèi)存中存儲了該線程以讀/寫共享變量的副本喳魏。JMM決定一個線程對共享變量的寫入何時對另一個線程可見。
在上圖中怀薛,如果線程A和線程B要進(jìn)行通信刺彩,必須要經(jīng)歷以下兩個步驟:
- 線程A把本地內(nèi)存中A中更新近的共享變量刷新到主內(nèi)存中去。
- 線程B到主內(nèi)存中去讀取線程A之前已更新過的共享變量枝恋。
2.內(nèi)存間的交互操作
關(guān)于主內(nèi)存與工作內(nèi)存之間具體的交互協(xié)議创倔,即一個變量如何從主內(nèi)存拷貝到工作內(nèi)存、如何從工作內(nèi)存同步回主內(nèi)存之類的實現(xiàn)細(xì)節(jié)焚碌,Java內(nèi)存模型中定義了以下8種操作來完成畦攘,虛擬機(jī)實現(xiàn)時必須保證下面提及的每一種操作都是原子的、不可再分的十电。
- lock(鎖定):作用于主內(nèi)存的變量知押,它把一個變量標(biāo)識為一條線程獨(dú)占的狀態(tài)。
- unlock(解鎖):作用于主內(nèi)存的變量鹃骂,它把一個處于鎖定狀態(tài)的變量釋放出來台盯,釋放后的變量才可以被其他線程鎖定。
- read(讀任废摺):作用于主內(nèi)存的變量静盅,它把一個變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便隨后的load動作使用寝殴。
- load(載入):作用于工作內(nèi)存的變量蒿叠,它把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中明垢。
- use(使用):作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個變量的值傳遞給執(zhí)行引擎栈虚,每當(dāng)虛擬機(jī)遇到一個需要使用到變量的值的字節(jié)碼指令時將會執(zhí)行這個操作袖外。
- assign(賦值):作用于工作內(nèi)存的變量,它把一個從執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量魂务,每當(dāng)虛擬機(jī)遇到一個給變量賦值的字節(jié)碼指令時執(zhí)行這個操作曼验。
- store(存儲):作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個變量的值傳送到主內(nèi)存中粘姜,以便隨后的write操作使用鬓照。
- write(寫入):作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中孤紧。
3.重排序
重排序是指編繹器和處理器為了優(yōu)化程序性能而對指令序列進(jìn)行重新排序的一種手段豺裆。重排序分為編譯器優(yōu)化的重排序,指令級并行的重排序和內(nèi)存系統(tǒng)的重排序号显。從Java源代碼到最終實際執(zhí)行的指令序列臭猜,會分別經(jīng)歷這3種重排序,如下所示:
上圖中的1屬于編繹器重排序押蚤,2和3屬于處理器重排序蔑歌。
3.1 內(nèi)存屏障
Memory Barrier,又稱內(nèi)存柵欄揽碘,是一個CPU指令次屠,基本上它是一條這樣的指令:
1、保證特定操作的執(zhí)行順序雳刺。
2劫灶、影響某些數(shù)據(jù)(或則是某條指令的執(zhí)行結(jié)果)的內(nèi)存可見性。
為了保證內(nèi)存可見性掖桦,Java編繹器在生成指令序列的適當(dāng)位置會插入內(nèi)存屏障指令來禁止特定類型的處理器重排序本昏。JMM把內(nèi)存屏障分為4類,如下所示:
3.2 happens-before
從jdk5開始枪汪,java使用新的JSR-133內(nèi)存模型涌穆,基于happens-before的概念來闡述操作之間的內(nèi)存可見性。
在JMM中料饥,如果一個操作的執(zhí)行結(jié)果需要對另一個操作可見,那么這兩個操作之間必須要存在happens-before關(guān)系朱监,這個的兩個操作既可以在同一個線程岸啡,也可以在不同的兩個線程中。
與程序員密切相關(guān)的happens-before規(guī)則如下:
1赫编、程序順序規(guī)則:一個線程中的每個操作巡蘸,happens-before于該線程中任意的后續(xù)操作奋隶。
2、監(jiān)視器鎖規(guī)則:對一個鎖的解鎖操作悦荒,happens-before于隨后對這個鎖的加鎖操作唯欣。
3、volatile域規(guī)則:對一個volatile域的寫操作搬味,happens-before于任意線程后續(xù)對這個volatile域的讀境氢。
4、傳遞性規(guī)則:如果 A happens-before B碰纬,且 B happens-before C萍聊,那么A happens-before C。
注意:兩個操作之間具有happens-before關(guān)系悦析,并不意味前一個操作必須要在后一個操作之前執(zhí)行寿桨!僅僅要求前一個操作的執(zhí)行結(jié)果,對于后一個操作是可見的强戴,且前一個操作按順序排在后一個操作之前亭螟。
3.3 數(shù)據(jù)依賴性
如果兩個操作訪問同一個變量,且這兩個操作中有一個為寫操作骑歹,此時這兩個操作之間就存在數(shù)據(jù)依賴性预烙。數(shù)據(jù)依賴性分為下列3種類型,如下所示:
對于上面的3種情況陵刹,只要重排序兩個操作的執(zhí)行順序默伍,程序的執(zhí)行結(jié)果就會被改變。編繹器和處理器在重排序時衰琐,會遵守數(shù)據(jù)依賴性也糊,而不會改變存在數(shù)據(jù)依賴關(guān)系的兩個操作的執(zhí)行順序。
3.4 as-if-serial
as-if-serial語義的意思是:不管怎么重排序(編繹器和處理器為了提高并行度)羡宙,(單線程)程序的執(zhí)行結(jié)果不能被改變狸剃。編繹器,runtime和處理器都必須遵守as-if-serial語義狗热。
3.5 示例
為了具體說明钞馁,請看下面計算圓面積的代碼示例:
double pi = 3.14; //A
double r = 1.0; //B
double area = pi * r * r; //C
上面3個操作的數(shù)據(jù)依賴關(guān)系如下圖所示:
A和C之間存在數(shù)據(jù)依賴關(guān)系,同時B和C之間也存在數(shù)據(jù)依賴關(guān)系匿刮。因此在最終執(zhí)行的指令序列中僧凰,C不能被重排序到A和B的前面(C排到A和B的前面,程序的結(jié)果將會被改變)熟丸。但A和B之間沒有數(shù)據(jù)依賴關(guān)系训措,編譯器和處理器可以重排序A和B之間的執(zhí)行順序。下圖是該程序的兩種執(zhí)行順序:
3.6 重排序?qū)Χ嗑€程的影響
先看如下示例代碼:
class ReorderExample {
int a = 0;
boolean flag = false;
public void writer() {
a = 1; //1
flag = true; //2
}
public void reader() {
if (flag) { //3
int i = a * a; //4
//s……
}
}
}
flag變量是個標(biāo)記,用來標(biāo)識變量a是否已被寫入绩鸣。這里假設(shè)有兩個線程A和B怀大,A首先執(zhí)行writer()方法,隨后B線程接著執(zhí)行reader()方法呀闻。線程B在執(zhí)行操作4時化借,能否看到線程A在操作1對共享變量a的寫入呢?
答案是:不一定能看到捡多。
由于操作1和操作2沒有數(shù)據(jù)依賴關(guān)系蓖康,編譯器和處理器可以對這兩個操作重排序;同樣局服,操作3和操作4沒有數(shù)據(jù)依賴關(guān)系钓瞭,編譯器和處理器也可以對這兩個操作重排序。讓我們先來看看淫奔,當(dāng)操作1和操作2重排序時山涡,可能會產(chǎn)生什么效果?請看下面的程序執(zhí)行時序圖唆迁,如下圖所示:
上圖中操作1和操作2做了重排序鸭丛。程序執(zhí)行時,線程A首先寫標(biāo)記變量flag唐责,隨后線B讀這個變量鳞溉。由于條件判斷為真,線程B將讀取變量a鼠哥。此時熟菲,變量a還沒有被線程A寫入,在里多線程程序的語義被重排序破壞了朴恳!
下面再讓我們看看抄罕,當(dāng)操作3和操作4重排序時會產(chǎn)生什么效果(借助這個重排序,可便說明控制依賴性)于颖。下面是操作3和操作4重排序后呆贿,程序執(zhí)行的時序圖,如下圖所示:
在程序中森渐,操作3和操作4存在控制依賴關(guān)系做入。當(dāng)代碼中存在控制依賴性時,會影響指令序列執(zhí)行的并行度同衣。為此竟块,編譯器和處理器會采用猜測Speculation)執(zhí)行來克服控制相關(guān)性對并行度的影響。以處理器的猜測執(zhí)行為例耐齐,執(zhí)行線程B的處理器可以提前讀取并計算a*a浪秘,然后把計算結(jié)果臨時保存到一個名為重排序緩沖(Reorder Buffer前弯,ROB)的硬件緩存中。當(dāng)操作3的條件判斷為真時秫逝,就把該計算結(jié)果寫入變量i中。
從上圖中我們可以看出询枚,猜測執(zhí)行實質(zhì)上對操作3和4做了重排序违帆。重排序在這里破壞了多線程程序的語義!
通過上例可以得出結(jié)論:在單線程程序中金蜀,對存在控制依賴的操作重排序刷后,不會改變執(zhí)行結(jié)果(這也是as-if-serial語義允許對存在控制依賴的操作做重排序的原因);但在多線程程序中渊抄,對存在控制依賴的操作重排序尝胆,可能會改變程序的執(zhí)行結(jié)果。