Java之JVM的深入探究(二)--內(nèi)存模型、可見性、指令重排序

接著上一篇我們已經(jīng)了解到了JVM的基本運(yùn)行流程以及內(nèi)存結(jié)構(gòu),在對JVM上已經(jīng)有了一個初步淺顯的認(rèn)知,其中在上一篇介紹JVM的時候我們介紹了JVM的內(nèi)存空間,本文將了解到j(luò)ava中變量的可見性及不同的java指令在并發(fā)訪問時可能發(fā)生的指令重排的情況,并且針對內(nèi)存空間的了解上我們在來探究一下JVM 的內(nèi)存模型,接下來讓我們一起進(jìn)一步探究:

Java變量的可見性:

所謂可見性,是指當(dāng)一條線程修改了共享變量的值,新值對于其他線程來說是可以立即得知的。

Java中有一個關(guān)鍵字volatile,到這里先簡單的介紹下volatile:

volatile是Java提供的一種輕量級的同步機(jī)制,在并發(fā)編程中,它也扮演著比較重要的角色。同synchronized相比(synchronized通常稱為重量級鎖),volatile更輕量級,相比使用synchronized所帶來的龐大開銷,倘若能恰當(dāng)?shù)暮侠淼氖褂胿olatile,自然是美事一樁。

那么,它有什么用呢?

這個答案其實(shí)就在上述java線程間通信機(jī)制中,我們想象一下,由于工作內(nèi)存這個中間層的出現(xiàn),線程1和線程2必然存在延遲的問題,例如線程1在工作內(nèi)存中更新了變量,但還沒刷新到主內(nèi)存,而此時線程2獲取到的變量值就是未更新的變量值,又或者線程1成功將變量更新到主內(nèi)存,但線程2依然使用自己工作內(nèi)存中的變量值,同樣會出問題。不管出現(xiàn)哪種情況都可能導(dǎo)致線程間的通信不能達(dá)到預(yù)期的目的。

例如以下例子:

//線程1
booleanstop =false;
while(!stop){ doSomething(); }
//線程2
stop =true;

這個經(jīng)典的例子表示線程2通過修改stop的值,控制線程1中斷,但在真實(shí)環(huán)境中可能會出現(xiàn)意想不到的結(jié)果,線程2在執(zhí)行之后,線程1并沒有立刻中斷甚至一直不會中斷。出現(xiàn)這種現(xiàn)象的原因就是線程2對線程1的變量更新無法第一時間獲取到。

但這一切等到Volatile出現(xiàn)后,再也不是問題,Volatile保證兩件事:
線程1工作內(nèi)存中的變量更新會強(qiáng)制立即寫入到主內(nèi)存;
線程2工作內(nèi)存中的變量會強(qiáng)制立即失效,這使得線程2必須去主內(nèi)存中獲取最新的變量值。
所以這就理解了Volatile保證了變量的可見性,因為線程1對變量的修改能第一時間讓線程2可見。

Java指令重排序:

到這里,關(guān)于指令排序我們先看下面一段代碼:
inta =0;booleanflag =false;|
//線程1
publicvoidwriter(){
a =1;
flag =true;
}

//線程2
publicvoidreader(){
if(flag) {
inti= a+1;
......
}
}

由代碼我們不難看出:線程1依次執(zhí)行a=1,flag=true;線程2判斷到flag==true后,設(shè)置i=a+1,根據(jù)代碼語義,我們可能會推斷此時i的值等于2,因為線程2在判斷flag==true時,線程1已經(jīng)執(zhí)行了a=1;所以i的值等于a+1=1+1=2;但真實(shí)情況卻不一定如此,引起這個問題的原因是線程1內(nèi)部的兩條語句a=1;flag=true;可能被重新排序執(zhí)行,如圖:

這就是指令重排序的簡單演示,兩個賦值語句盡管他們的代碼順序是一前一后,但真正執(zhí)行時卻不一定按照代碼順序執(zhí)行。你可能會說,有這個指令重排序那不是亂套了嗎?我寫的程序都不按我的代碼流程走,這怎么玩?這個你可以放心,你的程序不會亂套,因為java和CPU、內(nèi)存之間都有一套嚴(yán)格的指令重排序規(guī)則,哪些可以重排,哪些不能重排都有規(guī)矩的。下列流程演示了一個java程序從編譯到執(zhí)行會經(jīng)歷哪些重排序:

在上圖這個流程中第一步屬于編譯器重排查,編譯器重排序會按JMM的規(guī)范嚴(yán)格進(jìn)行,換言之編譯器重排序一般不會對程序的正確邏輯造成影響。第二、三步屬于處理器重排序,處理器重排序JMM就不好管了,怎么辦呢?它會要求java編譯器在生成指令時加入內(nèi)存屏障,內(nèi)存屏障是什么?你可以理解為一個不透風(fēng)的保護(hù)罩,把不能重排序的java指令保護(hù)起來,那么處理器在遇到內(nèi)存屏障保護(hù)的指令時就不會對它進(jìn)行重排序了。關(guān)于在哪些地方該加入內(nèi)存屏障,內(nèi)存屏障有哪些種類,各有什么作用,可以參考JVM規(guī)范相關(guān)資料。

下面介紹一下在同一個線程中,不會被重排序的邏輯:

這三種情況中,任意改變一個代碼的順序,結(jié)果都會大不相同,對于這樣的邏輯代碼,是不會被重排序的。注意這是指單線程中不會被重排序,如果在多線程環(huán)境下,還是會產(chǎn)生邏輯問題,例如前面舉的例子。

JVM內(nèi)存模型:

首先, 我們這里把Java堆稱為主內(nèi)存,并且Java堆是線程共享的,但是每一個線程都是有自己私有的內(nèi)存空間的,這里我們暫稱為每一個線程自己的工作空間。有了這個前提后我們想一想在java中一個線程要想另外一個線程進(jìn)行通信應(yīng)該怎么做? 說的更加確切一點(diǎn)就是一個java線程對于一個變量的更新是怎么通知到另外一個線程的呢? 有上一篇文章我們了解到j(luò)ava當(dāng)中的?實(shí)例對象、數(shù)組元素都存放在java堆中的.如果線程1要向線程2通信,一定會經(jīng)過下圖類似的流程:

簡單的解釋下該圖的:
線程1將自己工作內(nèi)存中的x更新為1并刷新到主內(nèi)存中;
線程2從主內(nèi)存中讀取到變量x=1,并且更新到自己的工作內(nèi)存中,因此,線程2讀取的x就是線程1更細(xì)后的值。
從上圖可知,java線程之間的通信都需要經(jīng)過主內(nèi)存,而主內(nèi)存與工作內(nèi)存之間的交互則需要java內(nèi)存模型(JMM)管理器,下圖中我們可以看到JMM是如何管理主內(nèi)存與工作內(nèi)存的:

需了解一:
當(dāng)線程1需要將一個更新后的變量值刷新到主內(nèi)存時,需要經(jīng)過以下兩個步驟:
????線程1工作內(nèi)存執(zhí)行store操作;
????主內(nèi)存執(zhí)行write操作;
????完成以上這兩步即可將工作內(nèi)存中的變量值刷新到主內(nèi)存,即線程1工作內(nèi)存和主內(nèi)存的變量值保持一致;

需了解二:? ??
當(dāng)線程2需要從主內(nèi)存中讀取變量的最新值時,同樣需要經(jīng)過兩個步驟:
????主內(nèi)存執(zhí)行read操作,將變量值從主內(nèi)存中讀取出來;
????線程2工作內(nèi)存執(zhí)行l(wèi)oad操作,將讀取出來的變量值更新到本地內(nèi)存的副本;
? ? 完成以上這兩步,線程2的變量和主內(nèi)存的變量值就保持一致了。

完成以上四個步驟:就實(shí)現(xiàn)了一個java線程要向另外一個線程進(jìn)行通信。接著我們在探究一下java中變量可見性的問題:

到此,本文所述已完畢

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容