溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

細(xì)談java同步之JMM(Java Memory Model)

發(fā)布時間:2020-10-19 14:10:10 來源:腳本之家 閱讀:99 作者:彤哥讀源碼 欄目:編程語言

簡介

Java內(nèi)存模型是在硬件內(nèi)存模型上的更高層的抽象,它屏蔽了各種硬件和操作系統(tǒng)訪問的差異性,保證了Java程序在各種平臺下對內(nèi)存的訪問都能達(dá)到一致的效果。

硬件內(nèi)存模型

在正式講解Java的內(nèi)存模型之前,我們有必要先了解一下硬件層面的一些東西。

在現(xiàn)代計算機(jī)的硬件體系中,CPU的運(yùn)算速度是非常快的,遠(yuǎn)遠(yuǎn)高于它從存儲介質(zhì)讀取數(shù)據(jù)的速度,這里的存儲介質(zhì)有很多,比如磁盤、光盤、網(wǎng)卡、內(nèi)存等,這些存儲介質(zhì)有一個很明顯的特點(diǎn)——距離CPU越近的存儲介質(zhì)往往越小越貴越快,距離CPU越遠(yuǎn)的存儲介質(zhì)往往越大越便宜越慢。

所以,在程序運(yùn)行的過程中,CPU大部分時間都浪費(fèi)在了磁盤IO、網(wǎng)絡(luò)通訊、數(shù)據(jù)庫訪問上,如果不想讓CPU在那里白白等待,我們就必須想辦法去把CPU的運(yùn)算能力壓榨出來,否則就會造成很大的浪費(fèi),而讓CPU同時去處理多項(xiàng)任務(wù)則是最容易想到的,也是被證明非常有效的壓榨手段,這也就是我們常說的“并發(fā)執(zhí)行”。

但是,讓CPU并發(fā)地執(zhí)行多項(xiàng)任務(wù)并不是那么容易實(shí)現(xiàn)的事,因?yàn)樗械倪\(yùn)算都不可能只依靠CPU的計算就能完成,往往還需要跟內(nèi)存進(jìn)行交互,如讀取運(yùn)算數(shù)據(jù)、存儲運(yùn)算結(jié)果等。

前面我們也說過了,CPU與內(nèi)存的交互往往是很慢的,所以這就要求我們要想辦法在CPU和內(nèi)存之間建立一種連接,使它們達(dá)到一種平衡,讓運(yùn)算能快速地進(jìn)行,而這種連接就是我們常說的“高速緩存”。

高速緩存的速度是非常接近CPU的,但是它的引入又帶來了新的問題,現(xiàn)代的CPU往往是有多個核心的,每個核心都有自己的緩存,而多個核心之間是不存在時間片的競爭的,它們可以并行地執(zhí)行,那么,怎么保證這些緩存與主內(nèi)存中的數(shù)據(jù)的一致性就成為了一個難題。

為了解決緩存一致性的問題,多個核心在訪問緩存時要遵循一些協(xié)議,在讀寫操作時根據(jù)協(xié)議來操作,這些協(xié)議有MSI、MESI、MOSI等,它們定義了何時應(yīng)該訪問緩存中的數(shù)據(jù)、何時應(yīng)該讓緩存失效、何時應(yīng)該訪問主內(nèi)存中的數(shù)據(jù)等基本原則。

細(xì)談java同步之JMM(Java Memory Model)

而隨著CPU能力的不斷提升,一層緩存就無法滿足要求了,就逐漸衍生出了多級緩存。

按照數(shù)據(jù)讀取順序和CPU的緊密程度,CPU的緩存可以分為一級緩存(L1)、二級緩存(L2)、三級緩存(L3),每一級緩存存儲的數(shù)據(jù)都是下一級的一部分。

這三種緩存的技術(shù)難度和制作成本是相對遞減的,容量也是相對遞增的。

所以,在有了多級緩存后,程序的運(yùn)行就變成了:

當(dāng)CPU要讀取一個數(shù)據(jù)的時候,先從一級緩存中查找,如果沒找到再從二級緩存中查找,如果沒找到再從三級緩存中查找,如果沒找到再從主內(nèi)存中查找,然后再把找到的數(shù)據(jù)依次加載到多級緩存中,下次再使用相關(guān)的數(shù)據(jù)直接從緩存中查找即可。

而加載到緩存中的數(shù)據(jù)也不是說用到哪個就加載哪個,而是加載內(nèi)存中連續(xù)的數(shù)據(jù),一般來說是加載連續(xù)的64個字節(jié),因此,如果訪問一個 long 類型的數(shù)組時,當(dāng)數(shù)組中的一個值被加載到緩存中時,另外 7 個元素也會被加載到緩存中,這就是“緩存行”的概念。

細(xì)談java同步之JMM(Java Memory Model)

緩存行雖然能極大地提高程序運(yùn)行的效率,但是在多線程對共享變量的訪問過程中又帶來了新的問題,也就是非常著名的“偽共享”。

關(guān)于偽共享的問題,我們這里就不展開講了,有興趣的可以看彤哥之前發(fā)布的【雜談 什么是偽共享(false sharing)?】章節(jié)的相關(guān)內(nèi)容。

除此之外,為了使CPU中的運(yùn)算單元能夠充分地被利用,CPU可能會對輸入的代碼進(jìn)行亂序執(zhí)行優(yōu)化,然后在計算之后再將亂序執(zhí)行的結(jié)果進(jìn)行重組,保證該結(jié)果與順序執(zhí)行的結(jié)果一致,但并不保證程序中各個語句計算的先后順序與代碼的輸入順序一致,因此,如果一個計算任務(wù)依賴于另一個計算任務(wù)的結(jié)果,那么其順序性并不能靠代碼的先后順序來保證。

與CPU的亂序執(zhí)行優(yōu)化類似,java虛擬機(jī)的即時編譯器也有類似的指令重排序優(yōu)化。

為了解決上面提到的多個緩存讀寫一致性以及亂序排序優(yōu)化的問題,這就有了內(nèi)存模型,它定義了共享內(nèi)存系統(tǒng)中多線程讀寫操作行為的規(guī)范。

Java內(nèi)存模型

Java內(nèi)存模型(Java Memory Model,JMM)是在硬件內(nèi)存模型基礎(chǔ)上更高層的抽象,它屏蔽了各種硬件和操作系統(tǒng)對內(nèi)存訪問的差異性,從而實(shí)現(xiàn)讓Java程序在各種平臺下都能達(dá)到一致的并發(fā)效果。

Java內(nèi)存模型定義了程序中各個變量的訪問規(guī)則,即在虛擬機(jī)中將變量存儲到內(nèi)存和從內(nèi)存中取出這樣的底層細(xì)節(jié)。這里所說的變量包括實(shí)例字段、靜態(tài)字段,但不包括局部變量和方法參數(shù),因?yàn)樗鼈兪蔷€程私有的,它們不會被共享,自然不存在競爭問題。

為了獲得更好的執(zhí)行效能,Java內(nèi)存模型并沒有限制執(zhí)行引擎使用處理器的特定寄存器或緩存來和主內(nèi)存進(jìn)行交互,也沒有限制即時編譯器調(diào)整代碼的執(zhí)行順序等這類權(quán)利。

Java內(nèi)存模型規(guī)定了所有的變量都存儲在主內(nèi)存中,這里的主內(nèi)存跟介紹硬件時所用的名字一樣,兩者可以類比,但此處僅指虛擬機(jī)中內(nèi)存的一部分。

除了主內(nèi)存,每條線程還有自己的工作內(nèi)存,此處可與CPU的高速緩存進(jìn)行類比。工作內(nèi)存中保存著該線程使用到的變量的主內(nèi)存副本的拷貝,線程對變量的操作都必須在工作內(nèi)存中進(jìn)行,包括讀取和賦值等,而不能直接讀寫主內(nèi)存中的變量,不同的線程之間也無法直接訪問對方工作內(nèi)存中的變量,線程間變量值的傳遞必須通過主內(nèi)存來完成。

線程、工作內(nèi)存、主內(nèi)存三者的關(guān)系如下圖所示:

細(xì)談java同步之JMM(Java Memory Model)

注意,這里所說的主內(nèi)存、工作內(nèi)存跟Java虛擬機(jī)內(nèi)存區(qū)域劃分中的堆、棧是不同層次的內(nèi)存劃分,如果兩者一定要勉強(qiáng)對應(yīng)起來,主內(nèi)存主要對應(yīng)于堆中對象的實(shí)例部分,而工作內(nèi)存主要對應(yīng)與虛擬機(jī)棧中的部分區(qū)域。

從更低層次來說,主內(nèi)存主要對應(yīng)于硬件內(nèi)存部分,工作內(nèi)存主要對應(yīng)于CPU的高速緩存和寄存器部分,但也不是絕對的,主內(nèi)存也可能存在于高速緩存和寄存器中,工作內(nèi)存也可能存在于硬件內(nèi)存中。

細(xì)談java同步之JMM(Java Memory Model)

內(nèi)存間的交互操作

關(guān)于主內(nèi)存與工作內(nèi)存之間具體的交互協(xié)議,Java內(nèi)存模型定義了以下8種具體的操作來完成:

(1)lock,鎖定,作用于主內(nèi)存的變量,它把主內(nèi)存中的變量標(biāo)識為一條線程獨(dú)占狀態(tài);

(2)unlock,解鎖,作用于主內(nèi)存的變量,它把鎖定的變量釋放出來,釋放出來的變量才可以被其它線程鎖定;

(3)read,讀取,作用于主內(nèi)存的變量,它把一個變量從主內(nèi)存?zhèn)鬏數(shù)焦ぷ鲀?nèi)存中,以便后續(xù)的load操作使用;

(4)load,載入,作用于工作內(nèi)存的變量,它把read操作從主內(nèi)存得到的變量放入工作內(nèi)存的變量副本中;

(5)use,使用,作用于工作內(nèi)存的變量,它把工作內(nèi)存中的一個變量傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個需要使用到變量的值的字節(jié)碼指令時將會執(zhí)行這個操作;

(6)assign,賦值,作用于工作內(nèi)存的變量,它把一個從執(zhí)行引擎接收到的變量賦值給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個給變量賦值的字節(jié)碼指令時使用這個操作;

(7)store,存儲,作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個變量的值傳遞到主內(nèi)存中,以便后續(xù)的write操作使用;

(8)write,寫入,作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存得到的變量的值放入到主內(nèi)存的變量中;

如果要把一個變量從主內(nèi)存復(fù)制到工作內(nèi)存,那就要按順序地執(zhí)行read和load操作,同樣地,如果要把一個變量從工作內(nèi)存同步回主內(nèi)存,就要按順序地執(zhí)行store和write操作。注意,這里只說明了要按順序,并沒有說一定要連續(xù),也就是說可以在read與load之間、store與write之間插入其它操作。比如,對主內(nèi)存中的變量a和b的訪問,可以按照以下順序執(zhí)行:

read a -> read b -> load b -> load a。

另外,Java內(nèi)存模型還定義了執(zhí)行上述8種操作的基本規(guī)則:

(1)不允許read和load、store和write操作之一單獨(dú)出現(xiàn),即不允許出現(xiàn)從主內(nèi)存讀取了而工作內(nèi)存不接受,或者從工作內(nèi)存回寫了但主內(nèi)存不接受的情況出現(xiàn);

(2)不允許一個線程丟棄它最近的assign操作,即變量在工作內(nèi)存變化了必須把該變化同步回主內(nèi)存;

(3)不允許一個線程無原因地(即未發(fā)生過assign操作)把一個變量從工作內(nèi)存同步回主內(nèi)存;

(4)一個新的變量必須在主內(nèi)存中誕生,不允許工作內(nèi)存中直接使用一個未被初始化(load或assign)過的變量,換句話說就是對一個變量的use和store操作之前必須執(zhí)行過load和assign操作;

(5)一個變量同一時刻只允許一條線程對其進(jìn)行l(wèi)ock操作,但lock操作可以被同一個線程執(zhí)行多次,多次執(zhí)行l(wèi)ock后,只有執(zhí)行相同次數(shù)的unlock操作,變量才能被解鎖。

(6)如果對一個變量執(zhí)行l(wèi)ock操作,將會清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個變量前,需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值;

(7)如果一個變量沒有被lock操作鎖定,則不允許對其執(zhí)行unlock操作,也不允許unlock一個其它線程鎖定的變量;

(8)對一個變量執(zhí)行unlock操作之前,必須先把此變量同步回主內(nèi)存中,即執(zhí)行store和write操作;

注意,這里的lock和unlock是實(shí)現(xiàn)synchronized的基礎(chǔ),Java并沒有把lock和unlock操作直接開放給用戶使用,但是卻提供了兩個更高層次的指令來隱式地使用這兩個操作,即moniterenter和moniterexit。

原子性、可見性、有序性

Java內(nèi)存模型就是為了解決多線程環(huán)境下共享變量的一致性問題,那么一致性包含哪些內(nèi)容呢?

一致性主要包含三大特性:原子性、可見性、有序性,下面我們就來看看Java內(nèi)存模型是怎么實(shí)現(xiàn)這三大特性的。

(1)原子性

原子性是指一段操作一旦開始就會一直運(yùn)行到底,中間不會被其它線程打斷,這段操作可以是一個操作,也可以是多個操作。

由Java內(nèi)存模型來直接保證的原子性操作包括read、load、user、assign、store、write這兩個操作,我們可以大致認(rèn)為基本類型變量的讀寫是具備原子性的。

如果應(yīng)用需要一個更大范圍的原子性,Java內(nèi)存模型還提供了lock和unlock這兩個操作來滿足這種需求,盡管不能直接使用這兩個操作,但我們可以使用它們更具體的實(shí)現(xiàn)synchronized來實(shí)現(xiàn)。

因此,synchronized塊之間的操作也是原子性的。

(2)可見性

可見性是指當(dāng)一個線程修改了共享變量的值,其它線程能立即感知到這種變化。

Java內(nèi)存模型是通過在變更修改后同步回主內(nèi)存,在變量讀取前從主內(nèi)存刷新變量值來實(shí)現(xiàn)的,它是依賴主內(nèi)存的,無論是普通變量還是volatile變量都是如此。

普通變量與volatile變量的主要區(qū)別是是否會在修改之后立即同步回主內(nèi)存,以及是否在每次讀取前立即從主內(nèi)存刷新。因此我們可以說volatile變量保證了多線程環(huán)境下變量的可見性,但普通變量不能保證這一點(diǎn)。

除了volatile之外,還有兩個關(guān)鍵字也可以保證可見性,它們是synchronized和final。

synchronized的可見性是由“對一個變量執(zhí)行unlock操作之前,必須先把此變量同步回主內(nèi)存中,即執(zhí)行store和write操作”這條規(guī)則獲取的。

final的可見性是指被final修飾的字段在構(gòu)造器中一旦被初始化完成,那么其它線程中就能看見這個final字段了。

(3)有序性

Java程序中天然的有序性可以總結(jié)為一句話:如果在本線程中觀察,所有的操作都是有序的;如果在另一個線程中觀察,所有的操作都是無序的。

前半句是指線程內(nèi)表現(xiàn)為串行的語義,后半句是指“指令重排序”現(xiàn)象和“工作內(nèi)存和主內(nèi)存同步延遲”現(xiàn)象。

Java中提供了volatile和synchronized兩個關(guān)鍵字來保證有序性。

volatile天然就具有有序性,因?yàn)槠浣怪嘏判颉?/p>

synchronized的有序性是由“一個變量同一時刻只允許一條線程對其進(jìn)行l(wèi)ock操作”這條規(guī)則獲取的。

先行發(fā)生原則(Happens-Before)

如果Java內(nèi)存模型的有序性都只依靠volatile和synchronized來完成,那么有一些操作就會變得很啰嗦,但是我們在編寫Java并發(fā)代碼時并沒有感受到,這是因?yàn)镴ava語言天然定義了一個“先行發(fā)生”原則,這個原則非常重要,依靠這個原則我們可以很容易地判斷在并發(fā)環(huán)境下兩個操作是否可能存在競爭沖突問題。

先行發(fā)生,是指操作A先行發(fā)生于操作B,那么操作A產(chǎn)生的影響能夠被操作B感知到,這種影響包括修改了共享內(nèi)存中變量的值、發(fā)送了消息、調(diào)用了方法等。

下面我們看看Java內(nèi)存模型定義的先行發(fā)生原則有哪些:

(1)程序次序原則

在一個線程內(nèi),按照程序書寫的順序執(zhí)行,書寫在前面的操作先行發(fā)生于書寫在后面的操作,準(zhǔn)確地講是控制流順序而不是代碼順序,因?yàn)橐紤]分支、循環(huán)等情況。

(2)監(jiān)視器鎖定原則

一個unlock操作先行發(fā)生于后面對同一個鎖的lock操作。

(3)volatile原則

對一個volatile變量的寫操作先行發(fā)生于后面對該變量的讀操作。

(4)線程啟動原則

對線程的start()操作先行發(fā)生于線程內(nèi)的任何操作。

(5)線程終止原則

線程中的所有操作先行發(fā)生于檢測到線程終止,可以通過Thread.join()、Thread.isAlive()的返回值檢測線程是否已經(jīng)終止。

(6)線程中斷原則

對線程的interrupt()的調(diào)用先行發(fā)生于線程的代碼中檢測到中斷事件的發(fā)生,可以通過Thread.interrupted()方法檢測是否發(fā)生中斷。

(7)對象終結(jié)原則

一個對象的初始化完成(構(gòu)造方法執(zhí)行結(jié)束)先行發(fā)生于它的finalize()方法的開始。

(8)傳遞性原則

如果操作A先行發(fā)生于操作B,操作B先行發(fā)生于操作C,那么操作A先行發(fā)生于操作C。

這里說的“先行發(fā)生”與“時間上的先發(fā)生”沒有必然的關(guān)系。

比如,下面的代碼:

int a = 0;
// 操作A:線程1對進(jìn)行賦值操作
a = 1;
// 操作B:線程2獲取a的值
int b = a;

如果線程1在時間順序上先對a進(jìn)行賦值,然后線程2再獲取a的值,這能說明操作A先行發(fā)生于操作B嗎?

顯然不能,因?yàn)榫€程2可能讀取的還是其工作內(nèi)存中的值,或者說線程1并沒有把a(bǔ)的值刷新回主內(nèi)存呢,這時候線程2讀取到的值可能還是0。

所以,“時間上的先發(fā)生”不一定“先行發(fā)生”。

再看一個例子:

// 同一個線程中
int i = 1;
int j = 2

根據(jù)第一條程序次序原則,int i = 1;先行發(fā)生于int j = 2;,但是由于處理器優(yōu)化,可能導(dǎo)致int j = 2;先執(zhí)行,但是這并不

影響先行發(fā)生原則的正確性,因?yàn)槲覀冊谶@個線程中并不會感知到這點(diǎn)。

所以,“先行發(fā)生”不一定“時間上先發(fā)生”。

總結(jié)

(1)硬件內(nèi)存架構(gòu)使得我們必須建立內(nèi)存模型來保證多線程環(huán)境下對共享內(nèi)存訪問的正確性;

(2)Java內(nèi)存模型定義了保證多線程環(huán)境下共享變量一致性的規(guī)則;

(3)Java內(nèi)存模型提供了工作內(nèi)存與主內(nèi)存交互的8大操作:lock、unlock、read、load、use、assign、store、write;

(4)Java內(nèi)存模型對原子性、可見性、有序性提供了一些實(shí)現(xiàn);

(5)先行發(fā)生的8大原則:程序次序原則、監(jiān)視器鎖定原則、volatile原則、線程啟動原則、線程終止原則、線程中斷原則、對象終結(jié)原則、傳遞性原則;

(6)先行發(fā)生不等于時間上的先發(fā)生;

以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持億速云。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI