JMM簡介
Java的內(nèi)存模型JMM(Java Memory
Model)JMM主要是為了規(guī)定了線程和內(nèi)存之間的一些關(guān)系拘泞。根據(jù)JMM的設(shè)計(jì),系統(tǒng)存在一個(gè)主內(nèi)存(Main
Memory),Java中所有實(shí)例變量都儲存在主存中,對于所有線程都是共享的摩泪。每條線程都有自己的工作內(nèi)存(Working
Memory),工作內(nèi)存由緩存和堆棧兩部分組成,緩存中保存的是主存中變量的拷貝,緩存可能并不總和主存同步,也就是緩存中變量的修改可能沒有立刻寫到主存中;堆棧中保存的是線程的局部變量,線程之間無法相互直接訪問堆棧中的變量
JMM是什么
JMM (Java Memory Model)是Java內(nèi)存模型,JMM定義了程序中各個(gè)共享變量的訪問規(guī)則,即在虛擬機(jī)中將變量存儲到內(nèi)存和從內(nèi)存讀取變量這樣的底層細(xì)節(jié).
為什么要設(shè)計(jì)JMM
屏蔽各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,以實(shí)現(xiàn)讓Java程序在各種平臺下都能達(dá)到一致的內(nèi)存訪問效果.
為什么要理解JMM
理解JMM是理解并發(fā)問題的基礎(chǔ).
主內(nèi)存,工作內(nèi)存和線程三者的交互關(guān)系
JMM規(guī)定了共享變量都存儲在主內(nèi)存中.每條線程還有自己的工作內(nèi)存,線程的工作內(nèi)存保存了主內(nèi)存的副本拷貝,對變量的操作在工作內(nèi)存中進(jìn)行,不能直接操作主內(nèi)存中的變量.不同線程間無法直接訪問對方的工作內(nèi)存變量,需要通過主內(nèi)存完成刀脏。如下圖:
JMM的基本概念
包括“并發(fā)、同步、主內(nèi)存箫津、本地內(nèi)存、重排序宰啦、內(nèi)存屏障苏遥、happens before規(guī)則、as-if-serial規(guī)則赡模、數(shù)據(jù)依賴性田炭、順序一致性模型、JMM的含義和意義”漓柑。下面一一講解 搞懂我們寫的代碼到底是怎么工作的
1教硫、并發(fā)
定義:即,并發(fā)(同時(shí))發(fā)生辆布。在操作系統(tǒng)中瞬矩,是指一個(gè)時(shí)間段中有幾個(gè)程序都處于已啟動(dòng)運(yùn)行到運(yùn)行完畢之間,且這幾個(gè)程序都是在同一個(gè)處理機(jī)上運(yùn)行锋玲,但任一個(gè)時(shí)刻點(diǎn)上只有一個(gè)程序在處理機(jī)上運(yùn)行景用。
并發(fā)需要處理兩個(gè)關(guān)鍵問題:線程之間如何通信及線程之間如何同步。
(01) 通信 —— 是指線程之間如何交換信息惭蹂。在命令式編程中伞插,線程之間的通信機(jī)制有兩種:共享內(nèi)存和消息傳遞。
(02) 同步—— 是指程序用于控制不同線程之間操作發(fā)生相對順序的機(jī)制盾碗。在Java中蜂怎,可以通過volatile,synchronized, 鎖等方式實(shí)現(xiàn)同步置尔。
-
1.1并發(fā)編程中的三個(gè)概念
- 1.1.1 原子性
原子是世界上的最小單位杠步,具有不可分割性氢伟。比如 a=0;(a非long和double類型) 這個(gè)操作是不可分割的幽歼,那么我們說這個(gè)操作時(shí)原子操作朵锣。再比如:a++; 這個(gè)操作實(shí)際是a = a + 1甸私;是可分割的诚些,所以他不是一個(gè)原子操作。非原子操作都會存在線程安全問題皇型,需要我們使用同步技術(shù)(sychronized)來讓它變成一個(gè)原子操作诬烹。一個(gè)操作是原子操作,那么我們稱它具有原子性弃鸦。java的concurrent包下提供了一些原子類绞吁,我們可以通過閱讀API來了解這些原子類的用法。比如:AtomicInteger唬格、AtomicLong家破、AtomicReference等。
- 1.1.1 原子性
舉個(gè)例子:
i = 0; //1
j = i ; //2
i++; //3
i = j + 1; //4
上面四個(gè)操作购岗,有哪個(gè)幾個(gè)是原子操作汰聋,那幾個(gè)不是?如果不是很理解喊积,可能會認(rèn)為都是原子性操作烹困,其實(shí)只有1才是原子操作,其余均不是乾吻。
1在Java中韭邓,對基本數(shù)據(jù)類型的變量和賦值操作都是原子性操作;
2中包含了兩個(gè)操作:讀取i溶弟,將i值賦值給j
3中包含了三個(gè)操作:讀取i值女淑、i + 1 、將+1結(jié)果賦值給i辜御;
4中同三一樣
- 1.1.2可見性
可見性鸭你,是指線程之間的可見性,一個(gè)線程修改的狀態(tài)對另一個(gè)線程是可見的擒权。也就是一個(gè)線程修改的結(jié)果袱巨。另一個(gè)線程馬上就能看到。
舉個(gè)例子:
//線程1執(zhí)行的代碼
int i = 0;
i = 10;
//線程2執(zhí)行的代碼
j = i;
假若執(zhí)行線程1的是CPU1碳抄,執(zhí)行線程2的是CPU2愉老。由上面的分析可知,當(dāng)線程1執(zhí)行 i = 10這句時(shí)剖效,會先把i的初始值加載到CPU1的高速緩存中嫉入,然后賦值為10焰盗,那么在CPU1的高速緩存當(dāng)中i的值變?yōu)?0了,卻沒有立即寫入到主存當(dāng)中咒林。此時(shí)線程2執(zhí)行 j = i熬拒,它會先去主存讀取i的值并加載到CPU2的緩存當(dāng)中,注意此時(shí)內(nèi)存當(dāng)中i的值還是0垫竞,那么就會使得j的值為0澎粟,而不是10。這就是可見性問題欢瞪,線程1對變量i修改了之后活烙,線程2沒有立即看到線程1修改的值。
在上面已經(jīng)分析了遣鼓,在多線程環(huán)境下啸盏,一個(gè)線程對共享變量的操作對其他線程是不可見的。
對于可見性譬正,Java提供了volatile關(guān)鍵字來保證可見性宫补。當(dāng)一個(gè)共享變量被volatile修飾時(shí)檬姥,它會保證修改的值會立即被更新到主存曾我,當(dāng)有其他線程需要讀取時(shí),它會去內(nèi)存中讀取新值健民。而普通的共享變量不能保證可見性抒巢,因?yàn)槠胀ü蚕碜兞勘恍薷闹螅裁磿r(shí)候被寫入主存是不確定的秉犹,當(dāng)其他線程去讀取時(shí)蛉谜,此時(shí)內(nèi)存中可能還是原來的舊值,因此無法保證可見性崇堵。另外型诚,通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時(shí)刻只有一個(gè)線程獲取鎖然后執(zhí)行同步代碼鸳劳,并且在釋放鎖之前會將對變量的修改刷新到主存當(dāng)中狰贯。因此可以保證可見性。
- 1.1.3有序性
即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行赏廓。
即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行:
//線程1執(zhí)行的代碼
int i = 0;
boolean flag = false;
i = 1; //語句1
flag = true; //語句2
上面代碼定義了一個(gè)int型變量涵紊,定義了一個(gè)boolean類型變量,然后分別對兩個(gè)變量進(jìn)行賦值操作幔摸。從代碼順序上看摸柄,語句1是在語句2前面的,那么JVM在真正執(zhí)行這段代碼的時(shí)候會保證語句1一定會在語句2前面執(zhí)行嗎既忆?不一定驱负,為什么呢嗦玖?這里可能會發(fā)生指令重排序(Instruction Reorder)。
在Java里面电媳,可以通過volatile關(guān)鍵字來保證一定的“有序性”踏揣。另外可以通過synchronized和Lock來保證有序性,很顯然匾乓,synchronized和Lock保證每個(gè)時(shí)刻是有一個(gè)線程執(zhí)行同步代碼捞稿,相當(dāng)于是讓線程順序執(zhí)行同步代碼,自然就保證了有序性拼缝。另外娱局,Java內(nèi)存模型具備一些先天的“有序性”,即不需要通過任何手段就能夠得到保證的有序性咧七,這個(gè)通常也稱為 happens-before 原則衰齐。
2、主內(nèi)存和本地內(nèi)存
主內(nèi)存 —— 即main memory继阻。在java中耻涛,實(shí)例域、靜態(tài)域和數(shù)組元素是線程之間共享的數(shù)據(jù)瘟檩,它們存儲在主內(nèi)存中抹缕。
本地內(nèi)存 —— 即local memory。 局部變量墨辛,方法定義參數(shù) 和 異常處理器參數(shù)是不會在線程之間共享的卓研,它們存儲在線程的本地內(nèi)存中。
-
2.1睹簇、 主內(nèi)存與工作內(nèi)存交互協(xié)議
JMM定義了8中操作來完成主內(nèi)存與工作內(nèi)存的交互奏赘。虛擬機(jī)保證每種操作都是原子的,不可再分的.8種操作分別是lock,unlock,read,load,use,assign,store,write.把一個(gè)變量復(fù)制到工作內(nèi)存,就要順序的執(zhí)行read和load操作,從工作內(nèi)存同步會主內(nèi)存,就要順序的執(zhí)行store和write操作.對一個(gè)變量執(zhí)行l(wèi)ock操作,將會清空工作內(nèi)存中此變量的值,在使用前,需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值對一個(gè)變量unlock操作前,必須先將此變量同步會主內(nèi)存.工作內(nèi)存中的volatile變量在每次使用前要刷新,執(zhí)行引擎看不到不一致的情況,volatile還禁止指令重排序
-
2.1.1內(nèi)存間交互操作
我們接著再來關(guān)注下變量從主內(nèi)存讀取到工作內(nèi)存,然后同步回工作內(nèi)存的細(xì)節(jié)太惠,這就是主內(nèi)存與工作內(nèi)存之間的交互協(xié)議磨淌。Java內(nèi)存模型定義了以下8種操作來完成,它們都是原子操作(除了對long和double類型的變量)凿渊。
lock(鎖定)
作用于主內(nèi)存中的變量梁只,它將一個(gè)變量標(biāo)志為一個(gè)線程獨(dú)占的狀態(tài)。unlock(解鎖)
作用于主內(nèi)存中的變量嗽元,解除變量的鎖定狀態(tài)敛纲,被解除鎖定狀態(tài)的變量才能被其他線程鎖定。read(讀取)
作用于主內(nèi)存中的變量剂癌,它把一個(gè)變量的值從主內(nèi)存中傳遞到工作內(nèi)存淤翔,以便進(jìn)行下一步的load操作。load(載入)
作用于工作內(nèi)存中的變量佩谷,它把read操作傳遞來的變量值放到工作內(nèi)存中的變量副本中旁壮。use(使用)
作用于工作內(nèi)存中的變量监嗜,這個(gè)操作把變量副本中的值傳遞給執(zhí)行引擎。當(dāng)執(zhí)行需要使用到變量值的字節(jié)碼指令的時(shí)候就會執(zhí)行這個(gè)操作抡谐。assign(賦值)
作用于工作內(nèi)存中的變量裁奇,接收執(zhí)行引擎?zhèn)鬟f過來的值,將其賦給工作內(nèi)存中的變量麦撵。當(dāng)執(zhí)行賦值的字節(jié)碼指令的時(shí)候就會執(zhí)行這個(gè)操作刽肠。store(存儲)
作用于工作內(nèi)存中的變量,它把工作內(nèi)存中的值傳遞到主內(nèi)存中來免胃,以便進(jìn)行下一步write操作音五。write(寫入)
作用于主內(nèi)存中的變量,它把store傳遞過來的值放到主內(nèi)存的變量中羔沙。-
2.1.2JMM對交互指令的約束
如果要把一個(gè)變量從主內(nèi)存中復(fù)制到工作內(nèi)存躺涝,就需要按順尋地執(zhí)行read和load操作,如果把變量從工作內(nèi)存中同步回主內(nèi)存中扼雏,就要按順序地執(zhí)行store和write操作坚嗜。
Java內(nèi)存模型只要求上述操作必須按順序執(zhí)行,而沒有保證必須是連續(xù)執(zhí)行诗充。也就是read和load之間苍蔬,store和write之間是可以插入其他指令的,如對主內(nèi)存中的變量a其障、b進(jìn)行訪問時(shí)银室,可能的順序是read a涂佃,read b励翼,load b, load a辜荠。Java內(nèi)存模型還規(guī)定了在執(zhí)行上述八種基本操作時(shí)汽抚,必須滿足如下規(guī)則:
不允許read和load、store和write操作之一單獨(dú)出現(xiàn)
不允許一個(gè)線程丟棄它的最近assign的操作伯病,即變量在工作內(nèi)存中改變了之后必須同步到主內(nèi)存中造烁。
不允許一個(gè)線程無原因地(沒有發(fā)生過任何assign操作)把數(shù)據(jù)從工作內(nèi)存同步回主內(nèi)存中。
一個(gè)新的變量只能在主內(nèi)存中誕生午笛,不允許在工作內(nèi)存中直接使用一個(gè)未被初始化(load或assign)的變量惭蟋。即就是對一個(gè)變量實(shí)施use和store操作之前,必須先執(zhí)行過了assign和load操作药磺。
一個(gè)變量在同一時(shí)刻只允許一條線程對其進(jìn)行l(wèi)ock操作告组,lock和unlock必須成對出現(xiàn)
如果對一個(gè)變量執(zhí)行l(wèi)ock操作,將會清空工作內(nèi)存中此變量的值癌佩,在執(zhí)行引擎使用這個(gè)變量前需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值
如果一個(gè)變量事先沒有被lock操作鎖定木缝,則不允許對它執(zhí)行unlock操作便锨;也不允許去unlock一個(gè)被其他線程鎖定的變量。
-
對一個(gè)變量執(zhí)行unlock操作之前我碟,必須先把此變量同步到主內(nèi)存中(執(zhí)行store和write操作)放案。
具體執(zhí)行流程圖:
主內(nèi)存到工作內(nèi)存流程圖.png
JMM規(guī)定如果要把一個(gè)變量從主內(nèi)存復(fù)制到工作內(nèi)存,那就要順序地執(zhí)行read和load操作矫俺,如果要把變量從工作內(nèi)存同步回主內(nèi)存吱殉,就要順序地執(zhí)行store和write操作。
注意厘托,Java內(nèi)存模型只要求上述兩個(gè)操作必須按順序執(zhí)行考婴,而沒有保證是連續(xù)執(zhí)行。也就是說催烘,read與load之間沥阱、store與write之間是可插入其他指令的,如對主內(nèi)存中的變量a伊群、b進(jìn)行訪問時(shí)考杉,一種可能出現(xiàn)順序是read a、read b舰始、load b崇棠、load a。
除了以上的順序約束以外丸卷,還規(guī)定了其他的約束:
a. 不允許read和load枕稀、store和write操作之一單獨(dú)出現(xiàn),即不允許一個(gè)變量從主內(nèi)存讀取了但工作內(nèi)存不接受谜嫉,或者從工作內(nèi)存發(fā)起回寫了但主內(nèi)存不接受的情況出現(xiàn)萎坷。
b. 不允許一個(gè)線程丟棄它的最近的assign操作,即變量在工作內(nèi)存中改變了之后必須把該變化同步回主內(nèi)存沐兰。
c. 不允許一個(gè)線程無原因地(沒有發(fā)生過任何assign操作)把數(shù)據(jù)從線程的工作內(nèi)存同步回主內(nèi)存中哆档。
d. 一個(gè)新的變量只能在主內(nèi)存中“誕生”,不允許在工作內(nèi)存中直接使用一個(gè)未被初始化(load或assign)的變量住闯,換句話說瓜浸,就是對一個(gè)變量實(shí)施use、store操作之前比原,必須先執(zhí)行過了assign和load操作插佛。
e. 一個(gè)變量在同一個(gè)時(shí)刻只允許一條線程對其進(jìn)行l(wèi)ock操作,但lock操作可以被同一條線程重復(fù)執(zhí)行多次量窘,多次執(zhí)行l(wèi)ock后雇寇,只有執(zhí)行相同次數(shù)的unlock操作,變量才會被解鎖。
f. 如果對一個(gè)變量執(zhí)行l(wèi)ock操作谢床,那將會清空工作內(nèi)存中此變量的值兄一,在執(zhí)行引擎使用這個(gè)變量前,需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值识腿。
g. 如果一個(gè)變量事先沒有被lock操作鎖定出革,那就不允許對它執(zhí)行unlock操作,也不允許去unlock一個(gè)被其他線程鎖定住的變量渡讼。 對一個(gè)變量執(zhí)行unlock操作之前骂束,必須先把此變量同步回主內(nèi)存中(執(zhí)行store、write操作)成箫。
3展箱、 重排序
計(jì)算機(jī)在執(zhí)行程序時(shí),為了提高性能蹬昌,編譯器和處理器的常常會對指令做重排混驰,一般分以下3種
- 編譯器優(yōu)化的重排
編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執(zhí)行順序皂贩。
- 指令并行的重排
現(xiàn)代處理器采用了指令級并行技術(shù)來將多條指令重疊執(zhí)行栖榨。如果不存在數(shù)據(jù)依賴性(即后一個(gè)執(zhí)行的語句無需依賴前面執(zhí)行的語句的結(jié)果),處理器可以改變語句對應(yīng)的機(jī)器指令的執(zhí)行順序
- 內(nèi)存系統(tǒng)的重排
由于處理器使用緩存和讀寫緩存沖區(qū)明刷,這使得加載(load)和存儲(store)操作看上去可能是在亂序執(zhí)行婴栽,因?yàn)槿壘彺娴拇嬖冢瑢?dǎo)致內(nèi)存與緩存的數(shù)據(jù)同步存在時(shí)間差辈末。
流程圖如下:
其中編譯器優(yōu)化的重排屬于編譯期重排愚争,指令并行的重排和內(nèi)存系統(tǒng)的重排屬于處理器重排,在多線程環(huán)境中挤聘,這些重排優(yōu)化可能會導(dǎo)致程序出現(xiàn)內(nèi)存可見性問題
舉個(gè)例子 下面代碼執(zhí)行順序是什么轰枝?
int x =1; //語句1
int y=2; //語句2
如果不了解重排序的同學(xué),可能說先執(zhí)行語句1,在執(zhí)行語句2。
其實(shí)在真正運(yùn)行的時(shí)候 可能是先執(zhí)行語句2在執(zhí)行語句1檬洞。
下面寫一段代碼來驗(yàn)證下重排序狸膏。這段代碼絕對是看完理解絕對能爽5天 沟饥。
/**
* @author hongwang.zhang
* @version: 1.0
* @date 2018/7/3118:25
* @see
**/
public class jmmReorder {
private static int x = 0, y = 0;
private static int a = 0, b =0;
public static void main(String[] args) throws InterruptedException {
int i = 0;
for(;;) {
i++;
x = 0; y = 0;
a = 0; b = 0;
Thread one = new Thread(new Runnable() {
public void run() {
//由于線程one先啟動(dòng)添怔,下面這句話讓它等一等線程two. 讀著可根據(jù)自己電腦的實(shí)際性能適當(dāng)調(diào)整等待時(shí)間.
shortWait(100000);
a = 1;
x = b;
}
});
Thread other = new Thread(new Runnable() {
public void run() {
b = 1;
y = a;
}
});
one.start();other.start();
one.join();other.join();
String result = "第" + i + "次 (" + x + "," + y + ")";
if(x == 0 && y == 0) {
System.err.println(result);
break;
} else {
System.out.println(result);
}
}
}
public static void shortWait(long interval){
long start = System.nanoTime();
long end;
do{
end = System.nanoTime();
}while(start + interval >= end);
}
}
出道題考考大家 以上代碼可能執(zhí)行的結(jié)果是:
A、1,0
B贤旷、0,1
C广料、1,1
D、0,0
E幼驶、以上均有可能
答案:69 請參考ascll編碼十進(jìn)制尋找答案
實(shí)驗(yàn)代碼是構(gòu)造一個(gè)循環(huán)艾杏,反復(fù)執(zhí)行上面的實(shí)例代碼,直到出現(xiàn)a=0且b=0的輸出為止盅藻。實(shí)驗(yàn)結(jié)果說明购桑,循環(huán)執(zhí)行到第13830次時(shí)輸出了(0,0)畅铭。
大多數(shù)現(xiàn)代微處理器都會采用將指令亂序執(zhí)行(out-of-order execution,簡稱OoOE或OOE)的方法勃蜘,在條件允許的情況下硕噩,直接運(yùn)行當(dāng)前有能力立即執(zhí)行的后續(xù)指令,避開獲取下一條指令所需數(shù)據(jù)時(shí)造成的等待缭贡。通過亂序執(zhí)行的技術(shù)炉擅,處理器可以大大提高執(zhí)行效率。
除了處理器阳惹,常見的Java運(yùn)行時(shí)環(huán)境的JIT編譯器也會做指令重排序操作谍失,即生成的機(jī)器指令與字節(jié)碼指令順序不一致。
如果你了解了重排序 那么就再來一份代碼告訴C等于幾
int a = 1;
int b = 2;
int c = a + b;
答案: C=3
原因: 不管怎么重排序(編譯器和處理器為了提高并行度),(單線程)程序的執(zhí)行結(jié)果不能被改變莹汤。
java內(nèi)存中的變量都有指針引用快鱼,上下文引用成鏈,這個(gè)鏈?zhǔn)遣粫淮騺y重排序的纲岭,只有沒有數(shù)據(jù)依賴關(guān)系的代碼攒巍,才會被沖排序,所以在單線程內(nèi)部重排序不會改變程序運(yùn)行結(jié)果
3荒勇、1數(shù)據(jù)依賴性
如果兩個(gè)操作訪問同一個(gè)變量柒莉,且這兩個(gè)操作中有一個(gè)為寫操作,此時(shí)這兩個(gè)操作之間就存在數(shù)據(jù)依賴性沽翔。數(shù)據(jù)依賴分下列三種類型:
名稱 | 代碼示例 | 說明 |
---|---|---|
寫后讀 | a = 1;b = a; | 寫一個(gè)變量之后兢孝,再讀這個(gè)位置。 |
寫后寫 | a = 1;a = 2; | 寫一個(gè)變量之后仅偎,再寫這個(gè)變量跨蟹。 |
讀后寫 | a = b;b = 1; | 讀一個(gè)變量之后,再寫這個(gè)變量橘沥。 |
上面三種情況窗轩,只要重排序兩個(gè)操作的執(zhí)行順序,程序的執(zhí)行結(jié)果將會被改變座咆。
前面提到過痢艺,編譯器和處理器可能會對操作做重排序。編譯器和處理器在重排序時(shí)介陶,會遵守?cái)?shù)據(jù)依賴性堤舒,編譯器和處理器不會改變存在數(shù)據(jù)依賴關(guān)系的兩個(gè)操作的執(zhí)行順序。
注意哺呜,這里所說的數(shù)據(jù)依賴性僅針對單個(gè)處理器中執(zhí)行的指令序列和單個(gè)線程中執(zhí)行的操作舌缤,不同處理器之間和不同線程之間的數(shù)據(jù)依賴性不被編譯器和處理器考慮。
總結(jié): 為了提高程序的并發(fā)度,從而提高性能国撵!但是對于多線程程序陵吸,重排序可能會導(dǎo)致程序執(zhí)行的結(jié)果不是我們需要的結(jié)果!因此介牙,在多線程環(huán)境下就需要我們通過“volatile走越,synchronize,鎖等方式”作出正確的實(shí)現(xiàn)同步,因?yàn)閱尉€程遵循as-if-serial語義
4耻瑟、as-if-serial語義
as-if-serial語義:
所有的動(dòng)作都可以為了優(yōu)化而被重排序旨指,但是必須保證它們重排序后的結(jié)果和程序代碼本身的應(yīng)有結(jié)果是一致的。Java編譯器喳整、運(yùn)行時(shí)和處理器都會保證Java在單線程下遵循as-if-serial語義谆构。
為了具體說明,請看下面計(jì)算圓面積的代碼示例:
double pi = 3.14; //A
double r = 1.0; //B
double area = pi * r * r; //C
上面三個(gè)操作的數(shù)據(jù)依賴關(guān)系如下圖所示:
如上圖所示框都,A 和 C 之間存在數(shù)據(jù)依賴關(guān)系搬素,同時(shí) B 和 C 之間也存在數(shù)據(jù)依賴關(guān)系。因此在最終執(zhí)行的指令序列中魏保,C 不能被重排序到 A 和 B 的前面(C 排到 A 和 B 的前面熬尺,程序的結(jié)果將會被改變)。但 A 和 B 之間沒有數(shù)據(jù)依賴關(guān)系谓罗,編譯器和處理器可以重排序 A 和 B 之間的執(zhí)行順序粱哼。下圖是該程序的兩種執(zhí)行順序:
了解后as-if-serial語義后如下代碼 X等于幾
public class Reordering {
public static void main(String[] args) {
int x, y;
x = 1;
try {
x = 2;
y = 0 / 0;
} catch (Exception e) {
} finally {
System.out.println("x = " + x);
}
}
}
答案是2
原因: 為保證as-if-serial語義,Java異常處理機(jī)制也會為重排序做一些特殊處理檩咱。例如在下面的代碼中揭措,y = 0 / 0可能會被重排序在x = 2之前執(zhí)行,為了保證最終不致于輸出x = 1的錯(cuò)誤結(jié)果刻蚯,JIT在重排序時(shí)會在catch語句中插入錯(cuò)誤代償代碼绊含,將x賦值為2,將程序恢復(fù)到發(fā)生異常時(shí)應(yīng)有的狀態(tài)炊汹。這種做法的確將異常捕捉的邏輯變得復(fù)雜了躬充,但是JIT的優(yōu)化的原則是,盡力優(yōu)化正常運(yùn)行下的代碼邏輯讨便,哪怕以catch塊邏輯變得復(fù)雜為代價(jià)充甚,畢竟,進(jìn)入catch塊內(nèi)是一種“異称髦樱”情況的表現(xiàn)津坑。
總結(jié): as-if-serial 語義把單線程程序保護(hù)了起來,遵守 as-if-serial 語義的編譯器傲霸,runtime 和處理器共同為編寫單線程程序的程序員創(chuàng)建了一個(gè)幻覺:單線程程序是按程序的順序來執(zhí)行的。as-if-serial 語義使單線程程序員無需擔(dān)心重排序會干擾他們,也無需擔(dān)心內(nèi)存可見性問題昙啄。
5穆役、 happens-before規(guī)則
從JDK5開始,Java使用新的JSR -133內(nèi)存模型梳凛。JSR-133提出了happens-before的概念耿币,通過這個(gè)概念來闡述操作之間的內(nèi)存可見性。
如果一個(gè)操作執(zhí)行的結(jié)果需要對另一個(gè)操作可見韧拒,那么這兩個(gè)操作之間必須存在happens-before關(guān)系淹接。這里提到的兩個(gè)操作既可以是在一個(gè)線程之內(nèi),也可以是在不同線程之間叛溢。兩個(gè)操作之間具有happens-before關(guān)系塑悼,并不意味著前一個(gè)操作必須要在后一個(gè)操作之前執(zhí)行!happens-before僅僅要求前一個(gè)操作(執(zhí)行的結(jié)果)對后一個(gè)操作可見楷掉。
常見的滿足happens- before原則的語法現(xiàn)象:
- 對象加鎖:對一個(gè)監(jiān)視器鎖的解鎖厢蒜,happens-before 于隨后對這個(gè)監(jiān)視器鎖的加鎖帘腹。
- volatile變量:對一個(gè)volatile域的寫痹扇,happens-before 于任意后續(xù)對這個(gè)volatile域的讀。
在java語言中大概有8大happens-before原則砖顷,分別如下:
- 程序次序規(guī)則(Pragram Order Rule):在一個(gè)線程內(nèi)草雕,按照程序代碼順序巷屿,書寫在前面的操作先行發(fā)生于書寫在后面的操作。準(zhǔn)確地說應(yīng)該是控制流順序而不是程序代碼順序墩虹,因?yàn)橐紤]分支攒庵、循環(huán)結(jié)構(gòu)。
一段代碼在單線程中執(zhí)行的結(jié)果是有序的败晴。注意是執(zhí)行結(jié)果浓冒,因?yàn)樘摂M機(jī)、處理器會對指令進(jìn)行重排序尖坤。雖然重排序了稳懒,但是并不會影響程序的執(zhí)行結(jié)果,所以程序最終執(zhí)行的結(jié)果與順序執(zhí)行的結(jié)果是一致的慢味。故而這個(gè)規(guī)則只對單線程有效场梆,在多線程環(huán)境下無法保證正確性。
int a = 3; //1
int b = a + 3; //2
這里的對b的賦值操作會用到變量a纯路,那么java的“單線程happen-before原則”就保證②的中的a的值一定是3或油,因?yàn)棰?書寫在②前面, ①對變量a的賦值操作對②一定可見。因?yàn)棰?中有用到①中的變量a驰唬,再加上java內(nèi)存模型提供了“單線程happen-before原則”顶岸,所以java虛擬機(jī)不許可操作系統(tǒng)對① ② 操作進(jìn)行指令重排序腔彰,即不可能有② 在①之前發(fā)生,
但是對于下面的代碼:
int a = 3;
int b = 4;
兩個(gè)語句直接沒有依賴關(guān)系辖佣,所以指令重排序可能發(fā)生霹抛,即對b的賦值可能先于對a的賦值。
- 監(jiān)視器規(guī)則(Monitor Lock Rule):
對某個(gè)鎖的unlock操作先行發(fā)生于后面對同一個(gè)鎖的lock操作卷谈。這里必須強(qiáng)調(diào)的是同一個(gè)鎖杯拐,這里的“后面”是指時(shí)間上的先后順序。也就是說世蔗,如果某個(gè)鎖已經(jīng)被lock了端逼,那么只有它被unlock之后,其他線程才能lock該鎖污淋。表現(xiàn)在代碼上顶滩,如果是某個(gè)同步方法,如果某個(gè)線程已經(jīng)進(jìn)入了該同步方法芙沥,只有當(dāng)這個(gè)線程退出了該同步方法(unlock操作)诲祸,別的線程才可以進(jìn)入該同步方法。
- volatile變量規(guī)則(Volatile Variable Rule):
對一個(gè)volatile變量的寫操作先行發(fā)生于對這個(gè)變量的讀操作而昨,這里的“后面”同樣是指時(shí)間上的先后順序救氯。也就是說,某個(gè)線程對volatile變量寫入某個(gè)值后歌憨,能立即被其它線程讀取到
- 線程啟動(dòng)規(guī)則(Thread Start Rule) :
Thread對象的start()方法先行發(fā)生于此線程的每一個(gè)動(dòng)作着憨。
- 線程終于規(guī)則(Thread Termination Rule) :
線程中的所有操作都先行發(fā)生于對此線程的終止檢測,我們可以通過Thread.join()方法結(jié)束务嫡,Thread.isAlive()的返回值等作段檢測到線程已經(jīng)終止執(zhí)行甲抖。
- 線程中斷規(guī)則(Thread Interruption Rule) :
對線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生,可以通過Thread.interrupted()方法檢測是否有中斷發(fā)生心铃。
- 對象終結(jié)規(guī)則(Finalizer Rule):
一個(gè)對象初始化完成(構(gòu)造方法執(zhí)行完成)先行發(fā)生于它的finalize()方法的開始准谚。
- 傳遞性(Transitivity):
如果操作A先行發(fā)生于操作B,操作B先行發(fā)生于操作C去扣,那就可以得出操作A先行發(fā)生于操作C的結(jié)論柱衔。
案例分析:
package com.test.volatiles;
import java.util.Vector;
/**
* @author hongwang.zhang
* @version: 1.0
* @date 2018/8/215:03
* @see
**/
public class Test2 {
private static Vector<Integer> vector = new Vector<Integer>();
public static void main(String[] args) {
while (true) {
for (int i = 0; i < 10; i++) {
vector.add(i);
}
Thread removeThread = new Thread(new Runnable() {
public void run() {
for (int i = 0; i < vector.size(); i++) {
vector.remove(i);
}
}
});
Thread getThread = new Thread(new Runnable() {
public void run() {
for (int i = 0; i < vector.size(); i++) {
// 嘗試加入首先判斷i是否在vector size范圍內(nèi),結(jié)果同樣報(bào)錯(cuò)愉棱,
// if (i < vector.size()) {
// continue;
// }
vector.get(i);
}
}
});
removeThread.start();
getThread.start();
//不要同時(shí)產(chǎn)生過多的線程唆铐,否則會導(dǎo)致操作系統(tǒng)假死
while (Thread.activeCount() > 20) ;
}
}
}
程序次序:不滿足,remove(i)與get(i)在控制流順序沒有先行發(fā)生關(guān)系奔滑;
管程鎖定:不滿足艾岂,remove(i)與get(i)方法都是synchronized修飾,但各自持有不同的鎖朋其,不滿足管程鎖定要求的同一個(gè)鎖王浴;
volatile變量:不滿足脆炎,沒有volatile修飾變量,無視叼耙;
線程啟動(dòng):不滿足腕窥,removeThread.start()先與vector.remove(i)粒没,getThread.start()先于vector.get(i)筛婉,但后兩者明顯沒有關(guān)系;
線程終止:不滿足癞松;
線程中斷:不滿足爽撒;
對象終結(jié):不滿足,不存在對象終結(jié)的關(guān)系响蓉;
傳遞性:不滿足硕勿,加入size()驗(yàn)證作為參考,假定A是remove()枫甲,B是size()驗(yàn)證源武,C是get(),B先于C想幻,但A可能介乎于BC之間粱栖,也可能在B之前。因此不符合傳遞性脏毯。
結(jié)論:Vector作為相對線程安全對象闹究,其單個(gè)方法帶Synchronized修飾,是相對線程安全的食店,但Vector方法之間不是線程安全的渣淤,不能保證多個(gè)方法作用下的數(shù)據(jù)一致性。執(zhí)行例子get()會報(bào)錯(cuò):java.lang.ArrayIndexOutOfBoundsException吉嫩。
時(shí)間上的先后順序”與“先行發(fā)生”之間有什么不同:
private int value=0价认;
pubilc void setValue(int value){
this.value=value;
} public int getValue(){
return value自娩;
}
以上顯示的是一組再普通不過的getter/setter方法用踩,假設(shè)存在線程A和B,線程A先(時(shí)間上的先后)調(diào)用了“setValue(1)”椒功,然后線程B調(diào)用了同一個(gè)對象的“getValue()”捶箱,那么線程B收到的返回值是什么?
我們依次分析一下先行發(fā)生原則中的各項(xiàng)規(guī)則动漾,由于兩個(gè)方法分別由線程A和線程B調(diào)用丁屎,不在一個(gè)線程中,所以程序次序規(guī)則在這里不適用旱眯;由于沒有同步塊晨川,自然就不會發(fā)生lock和unlock操作证九,所以管程鎖定規(guī)則不適用;由于value變量沒有被volatile關(guān)鍵字修飾共虑,所以volatile變量規(guī)則不適用愧怜;后面的線程啟動(dòng)、 終止妈拌、 中斷規(guī)則和對象終結(jié)規(guī)則也和這里完全沒有關(guān)系拥坛。 因?yàn)闆]有一個(gè)適用的先行發(fā)生規(guī)則,所以最后一條傳遞性也無從談起尘分,因此我們可以判定盡管線程A在操作時(shí)間上先于線程B猜惋,但是無法確定線程B中“getValue()”方法的返回結(jié)果,換句話說培愁,這里面的操作不是線程安全的著摔。
那怎么修復(fù)這個(gè)問題呢?我們至少有兩種比較簡單的方案可以選擇:要么把getter/setter方法都定義為synchronized方法定续,這樣就可以套用管程鎖定規(guī)則谍咆;要么把value定義為volatile變量,由于setter方法對value的修改不依賴value的原值私股,滿足volatile關(guān)鍵字使用場景摹察,這樣就可以套用volatile變量規(guī)則來實(shí)現(xiàn)先行發(fā)生關(guān)系。
通過上面的例子庇茫,我們可以得出結(jié)論:一個(gè)操作“時(shí)間上的先發(fā)生”不代表這個(gè)操作會是“先行發(fā)生”港粱,那如果一個(gè)操作“先行發(fā)生”是否就能推導(dǎo)出這個(gè)操作必定是“時(shí)間上的先發(fā)生”呢?很遺憾旦签,這個(gè)推論也是不成立的查坪,一個(gè)典型的例子就是多次提到的“指令重排序”,演示例子如下代碼所示:
//以下操作在同一個(gè)線程中執(zhí)行
int i=1宁炫;
int j=2偿曙;
以上代碼的兩條賦值語句在同一個(gè)線程之中,根據(jù)程序次序規(guī)則羔巢,“int i=1”的操作先行發(fā)生于“int j=2”望忆,但是“int j=2”的代碼完全可能先被處理器執(zhí)行,這并不影響先行發(fā)生原則的正確性竿秆,因?yàn)槲覀冊谶@條線程之中沒有辦法感知到這點(diǎn)启摄。
上面兩個(gè)例子綜合起來證明了一個(gè)結(jié)論:時(shí)間先后順序與先行發(fā)生原則之間基本沒有太大的關(guān)系,所以我們衡量并發(fā)安全問題的時(shí)候不要受到時(shí)間順序的干擾幽钢,一切必須以先行發(fā)生原則為準(zhǔn)歉备。
6、內(nèi)存屏障
處理器都支持一定的內(nèi)存屏障(memory barrier)或柵欄(fence)來控制重排序和數(shù)據(jù)在不同的處理器間的可見性匪燕。將CPU和內(nèi)存間的數(shù)據(jù)存取操作分為load和store蕾羊。例如喧笔,CPU將數(shù)據(jù)寫回時(shí),會將store請求放入write buffer中等待flush到內(nèi)存龟再,可以通過插入barrier的方式防止這個(gè)store請求與其他的請求重排序书闸、保證數(shù)據(jù)的可見性±眨可以用一個(gè)生活中的例子類比屏障浆劲,例如坐地鐵的斜坡式電梯時(shí),大家按順序進(jìn)入電梯截碴,但是會有一些人從左側(cè)繞過去梳侨,這樣出電梯時(shí)順序就不相同了蛉威,如果有一個(gè)人攜帶了一個(gè)大的行李堵住了(屏障)日丹,則后面的人就不能繞過去了:)。另外這里的barrier和GC中用到的write barrier是不同的概念蚯嫌。
下面是常見處理器允許的重排序類型的列表
上面我們說了處理器會發(fā)生指令重排,現(xiàn)在來簡單的看看常見處理器允許的重排規(guī)則,換言之就是處理器可以對那些指令進(jìn)行順序調(diào)整:
處理器 | Load-Load | Load-Store | Store-Store | Store-Load | 數(shù)據(jù)依賴 |
---|---|---|---|---|---|
x86 | N | N | N | Y | N |
PowerPC | Y | Y | Y | Y | N |
ia64 | Y | Y | Y | Y | N |
上表單元格中的 “N” 表示處理器不允許兩個(gè)操作重排序哲虾,“Y” 表示允許重排序。
從上表我們可以看出:常見的處理器都允許 Store-Load 重排序择示;常見的處理器都不允許對存在數(shù)據(jù)依賴的操作做重排序束凑。 x86 擁有相對較強(qiáng)的處理器內(nèi)存模型,它們僅允許對寫-讀操作做重排序(因?yàn)樗鼈兌际褂昧藢懢彌_區(qū))栅盲。
※注1:上表中的 x86 包括 x64 及 AMD64汪诉。=
※注2:由于 ARM 處理器的內(nèi)存模型與 PowerPC 處理器的內(nèi)存模型非常類似,本文將忽略它谈秫。
※注3:數(shù)據(jù)依賴性后文會專門說明扒寄。
先簡單了解兩個(gè)指令:
- Store:將處理器緩存的數(shù)據(jù)刷新到內(nèi)存中。
- Load:將內(nèi)存存儲的數(shù)據(jù)拷貝到處理器的緩存中拟烫。
表格中的Y表示前后兩個(gè)操作允許重排,N則表示不允許重排.與這些規(guī)則對應(yīng)是的禁止重排的內(nèi)存屏障.
注意:處理器和編譯都會遵循數(shù)據(jù)依賴性,不會改變存在數(shù)據(jù)依賴關(guān)系的兩個(gè)操作的順序.所謂的數(shù)據(jù)依賴性就是如果兩個(gè)操作訪問同一個(gè)變量,且這兩個(gè)操作中有一個(gè)是寫操作,那么久可以稱這兩個(gè)操作存在數(shù)據(jù)依賴性.舉個(gè)簡單例子:
a=100;//write
b=a;//read
或者
a=100;//write
a=2000;//write
或者
a=b;//read
b=12;//write
以上所示的,兩個(gè)操作之間不能發(fā)生重排,這是處理器和編譯所必須遵循的.當(dāng)然這里指的是發(fā)生在單個(gè)處理器或單個(gè)線程中.
內(nèi)存屏障的分類
幾乎所有的處理器都支持一定粗粒度的barrier指令该编,通常叫做Fence(柵欄、圍墻)硕淑,能夠保證在fence之前發(fā)起的load和store指令都能嚴(yán)格的和fence之后的load和store保持有序课竣。通常按照用途會分為下面四種barrier
屏障類型 | 指令市里 | 說明 |
---|---|---|
LoadLoad Barriers | Load1; LoadLoad; Load2 | 確保Load1數(shù)據(jù)的裝載,之前于Load2及所有后續(xù)裝載指令的裝載置媳。 |
StoreStore Barriers | Store1; StoreStore; Store2 | 確保Store1數(shù)據(jù)對其他處理器可見(刷新到內(nèi)存)于樟,之前于Store2及所有后續(xù)存儲指令的存儲。 |
LoadStore Barriers | Load1; LoadStore; Store2 | 確保Load1數(shù)據(jù)裝載拇囊,之前于Store2及所有后續(xù)的存儲指令刷新到內(nèi)存迂曲。 |
StoreLoad Barriers | Store1; StoreLoad; Load2 | 確保Store1數(shù)據(jù)對其他處理器變得可見(指刷新到內(nèi)存),之前于Load2及所有后續(xù)裝載指令的裝載寂拆。StoreLoad Barriers會使該屏障之前的所有內(nèi)存訪問指令(存儲和裝載指令)完成之后奢米,才執(zhí)行該屏障之后的內(nèi)存訪問指令抓韩。 |
StoreLoad Barriers同時(shí)具備其他三個(gè)屏障的效果,因此也稱之為全能屏障(mfence)鬓长,是目前大多數(shù)處理器所支持的谒拴;但是相對其他屏障,該屏障的開銷相對昂貴涉波。
內(nèi)存屏障對性能的影響(Performance Impact of Memory Barriers)
內(nèi)存屏障阻止了 CPU 執(zhí)行很多隱藏內(nèi)存延遲的技術(shù)英上,因此有它們有顯著的性能開銷,必須考慮啤覆。為了達(dá)到最大性能苍日,最好對問題建模,這樣處理器可以做工作單元窗声,然后讓所有必須的內(nèi)存屏障在工作單元的邊界上發(fā)生相恃。采用這種方法允許處理器不受限制地優(yōu)化工作單元。把必須的內(nèi)存屏障分組是有益的笨觅,那樣拦耐,在第一個(gè)之后的 buffer 刷新的開銷會小點(diǎn),因?yàn)闆]有工作需要進(jìn)行重新填充它见剩。
總結(jié): 通過內(nèi)存屏障可以禁止特定類型處理器的重排序杀糯,從而讓程序按我們預(yù)想的流程去執(zhí)行。