Java內(nèi)存模型是在硬件內(nèi)存模型上的更高層的抽象,它屏蔽了各種硬件和操作系統(tǒng)訪問(wèn)的差異性,保證了Java程序在各種平臺(tái)下對(duì)內(nèi)存的訪問(wèn)都能達(dá)到一致的效果。
創(chuàng)新互聯(lián)公司專注于企業(yè)全網(wǎng)整合營(yíng)銷推廣、網(wǎng)站重做改版、肥東網(wǎng)站定制設(shè)計(jì)、自適應(yīng)品牌網(wǎng)站建設(shè)、H5響應(yīng)式網(wǎng)站、成都做商城網(wǎng)站、集團(tuán)公司官網(wǎng)建設(shè)、外貿(mào)網(wǎng)站建設(shè)、高端網(wǎng)站制作、響應(yīng)式網(wǎng)頁(yè)設(shè)計(jì)等建站業(yè)務(wù),價(jià)格優(yōu)惠性價(jià)比高,為肥東等各大城市提供網(wǎng)站開(kāi)發(fā)制作服務(wù)。在正式講解Java的內(nèi)存模型之前,我們有必要先了解一下硬件層面的一些東西。
在現(xiàn)代計(jì)算機(jī)的硬件體系中,CPU的運(yùn)算速度是非常快的,遠(yuǎn)遠(yuǎn)高于它從存儲(chǔ)介質(zhì)讀取數(shù)據(jù)的速度,這里的存儲(chǔ)介質(zhì)有很多,比如磁盤(pán)、光盤(pán)、網(wǎng)卡、內(nèi)存等,這些存儲(chǔ)介質(zhì)有一個(gè)很明顯的特點(diǎn)——距離CPU越近的存儲(chǔ)介質(zhì)往往越小越貴越快,距離CPU越遠(yuǎn)的存儲(chǔ)介質(zhì)往往越大越便宜越慢。
所以,在程序運(yùn)行的過(guò)程中,CPU大部分時(shí)間都浪費(fèi)在了磁盤(pán)IO、網(wǎng)絡(luò)通訊、數(shù)據(jù)庫(kù)訪問(wèn)上,如果不想讓CPU在那里白白等待,我們就必須想辦法去把CPU的運(yùn)算能力壓榨出來(lái),否則就會(huì)造成很大的浪費(fèi),而讓CPU同時(shí)去處理多項(xiàng)任務(wù)則是最容易想到的,也是被證明非常有效的壓榨手段,這也就是我們常說(shuō)的“并發(fā)執(zhí)行”。
但是,讓CPU并發(fā)地執(zhí)行多項(xiàng)任務(wù)并不是那么容易實(shí)現(xiàn)的事,因?yàn)樗械倪\(yùn)算都不可能只依靠CPU的計(jì)算就能完成,往往還需要跟內(nèi)存進(jìn)行交互,如讀取運(yùn)算數(shù)據(jù)、存儲(chǔ)運(yùn)算結(jié)果等。
前面我們也說(shuō)過(guò)了,CPU與內(nèi)存的交互往往是很慢的,所以這就要求我們要想辦法在CPU和內(nèi)存之間建立一種連接,使它們達(dá)到一種平衡,讓運(yùn)算能快速地進(jìn)行,而這種連接就是我們常說(shuō)的“高速緩存”。
高速緩存的速度是非常接近CPU的,但是它的引入又帶來(lái)了新的問(wèn)題,現(xiàn)代的CPU往往是有多個(gè)核心的,每個(gè)核心都有自己的緩存,而多個(gè)核心之間是不存在時(shí)間片的競(jìng)爭(zhēng)的,它們可以并行地執(zhí)行,那么,怎么保證這些緩存與主內(nèi)存中的數(shù)據(jù)的一致性就成為了一個(gè)難題。
為了解決緩存一致性的問(wèn)題,多個(gè)核心在訪問(wèn)緩存時(shí)要遵循一些協(xié)議,在讀寫(xiě)操作時(shí)根據(jù)協(xié)議來(lái)操作,這些協(xié)議有MSI、MESI、MOSI等,它們定義了何時(shí)應(yīng)該訪問(wèn)緩存中的數(shù)據(jù)、何時(shí)應(yīng)該讓緩存失效、何時(shí)應(yīng)該訪問(wèn)主內(nèi)存中的數(shù)據(jù)等基本原則。
而隨著CPU能力的不斷提升,一層緩存就無(wú)法滿足要求了,就逐漸衍生出了多級(jí)緩存。
按照數(shù)據(jù)讀取順序和CPU的緊密程度,CPU的緩存可以分為一級(jí)緩存(L1)、二級(jí)緩存(L2)、三級(jí)緩存(L3),每一級(jí)緩存存儲(chǔ)的數(shù)據(jù)都是下一級(jí)的一部分。
這三種緩存的技術(shù)難度和制作成本是相對(duì)遞減的,容量也是相對(duì)遞增的。
所以,在有了多級(jí)緩存后,程序的運(yùn)行就變成了:
當(dāng)CPU要讀取一個(gè)數(shù)據(jù)的時(shí)候,先從一級(jí)緩存中查找,如果沒(méi)找到再?gòu)亩?jí)緩存中查找,如果沒(méi)找到再?gòu)娜?jí)緩存中查找,如果沒(méi)找到再?gòu)闹鲀?nèi)存中查找,然后再把找到的數(shù)據(jù)依次加載到多級(jí)緩存中,下次再使用相關(guān)的數(shù)據(jù)直接從緩存中查找即可。
而加載到緩存中的數(shù)據(jù)也不是說(shuō)用到哪個(gè)就加載哪個(gè),而是加載內(nèi)存中連續(xù)的數(shù)據(jù),一般來(lái)說(shuō)是加載連續(xù)的64個(gè)字節(jié),因此,如果訪問(wèn)一個(gè) long 類型的數(shù)組時(shí),當(dāng)數(shù)組中的一個(gè)值被加載到緩存中時(shí),另外 7 個(gè)元素也會(huì)被加載到緩存中,這就是“緩存行”的概念。
緩存行雖然能極大地提高程序運(yùn)行的效率,但是在多線程對(duì)共享變量的訪問(wèn)過(guò)程中又帶來(lái)了新的問(wèn)題,也就是非常著名的“偽共享”。
關(guān)于偽共享的問(wèn)題,我們這里就不展開(kāi)講了,有興趣的可以看彤哥之前發(fā)布的【雜談 什么是偽共享(false sharing)?】章節(jié)的相關(guān)內(nèi)容。
除此之外,為了使CPU中的運(yùn)算單元能夠充分地被利用,CPU可能會(huì)對(duì)輸入的代碼進(jìn)行亂序執(zhí)行優(yōu)化,然后在計(jì)算之后再將亂序執(zhí)行的結(jié)果進(jìn)行重組,保證該結(jié)果與順序執(zhí)行的結(jié)果一致,但并不保證程序中各個(gè)語(yǔ)句計(jì)算的先后順序與代碼的輸入順序一致,因此,如果一個(gè)計(jì)算任務(wù)依賴于另一個(gè)計(jì)算任務(wù)的結(jié)果,那么其順序性并不能靠代碼的先后順序來(lái)保證。
與CPU的亂序執(zhí)行優(yōu)化類似,java虛擬機(jī)的即時(shí)編譯器也有類似的指令重排序優(yōu)化。
為了解決上面提到的多個(gè)緩存讀寫(xiě)一致性以及亂序排序優(yōu)化的問(wèn)題,這就有了內(nèi)存模型,它定義了共享內(nèi)存系統(tǒng)中多線程讀寫(xiě)操作行為的規(guī)范。
Java內(nèi)存模型(Java Memory Model,JMM)是在硬件內(nèi)存模型基礎(chǔ)上更高層的抽象,它屏蔽了各種硬件和操作系統(tǒng)對(duì)內(nèi)存訪問(wèn)的差異性,從而實(shí)現(xiàn)讓Java程序在各種平臺(tái)下都能達(dá)到一致的并發(fā)效果。
Java內(nèi)存模型定義了程序中各個(gè)變量的訪問(wèn)規(guī)則,即在虛擬機(jī)中將變量存儲(chǔ)到內(nèi)存和從內(nèi)存中取出這樣的底層細(xì)節(jié)。這里所說(shuō)的變量包括實(shí)例字段、靜態(tài)字段,但不包括局部變量和方法參數(shù),因?yàn)樗鼈兪蔷€程私有的,它們不會(huì)被共享,自然不存在競(jìng)爭(zhēng)問(wèn)題。
為了獲得更好的執(zhí)行效能,Java內(nèi)存模型并沒(méi)有限制執(zhí)行引擎使用處理器的特定寄存器或緩存來(lái)和主內(nèi)存進(jìn)行交互,也沒(méi)有限制即時(shí)編譯器調(diào)整代碼的執(zhí)行順序等這類權(quán)利。
Java內(nèi)存模型規(guī)定了所有的變量都存儲(chǔ)在主內(nèi)存中,這里的主內(nèi)存跟介紹硬件時(shí)所用的名字一樣,兩者可以類比,但此處僅指虛擬機(jī)中內(nèi)存的一部分。
除了主內(nèi)存,每條線程還有自己的工作內(nèi)存,此處可與CPU的高速緩存進(jìn)行類比。工作內(nèi)存中保存著該線程使用到的變量的主內(nèi)存副本的拷貝,線程對(duì)變量的操作都必須在工作內(nèi)存中進(jìn)行,包括讀取和賦值等,而不能直接讀寫(xiě)主內(nèi)存中的變量,不同的線程之間也無(wú)法直接訪問(wèn)對(duì)方工作內(nèi)存中的變量,線程間變量值的傳遞必須通過(guò)主內(nèi)存來(lái)完成。
線程、工作內(nèi)存、主內(nèi)存三者的關(guān)系如下圖所示:
注意,這里所說(shuō)的主內(nèi)存、工作內(nèi)存跟Java虛擬機(jī)內(nèi)存區(qū)域劃分中的堆、棧是不同層次的內(nèi)存劃分,如果兩者一定要勉強(qiáng)對(duì)應(yīng)起來(lái),主內(nèi)存主要對(duì)應(yīng)于堆中對(duì)象的實(shí)例部分,而工作內(nèi)存主要對(duì)應(yīng)與虛擬機(jī)棧中的部分區(qū)域。
從更低層次來(lái)說(shuō),主內(nèi)存主要對(duì)應(yīng)于硬件內(nèi)存部分,工作內(nèi)存主要對(duì)應(yīng)于CPU的高速緩存和寄存器部分,但也不是絕對(duì)的,主內(nèi)存也可能存在于高速緩存和寄存器中,工作內(nèi)存也可能存在于硬件內(nèi)存中。
關(guān)于主內(nèi)存與工作內(nèi)存之間具體的交互協(xié)議,Java內(nèi)存模型定義了以下8種具體的操作來(lái)完成:
(1)lock,鎖定,作用于主內(nèi)存的變量,它把主內(nèi)存中的變量標(biāo)識(shí)為一條線程獨(dú)占狀態(tài);
(2)unlock,解鎖,作用于主內(nèi)存的變量,它把鎖定的變量釋放出來(lái),釋放出來(lái)的變量才可以被其它線程鎖定;
(3)read,讀取,作用于主內(nèi)存的變量,它把一個(gè)變量從主內(nèi)存?zhèn)鬏數(shù)焦ぷ鲀?nèi)存中,以便后續(xù)的load操作使用;
(4)load,載入,作用于工作內(nèi)存的變量,它把read操作從主內(nèi)存得到的變量放入工作內(nèi)存的變量副本中;
(5)use,使用,作用于工作內(nèi)存的變量,它把工作內(nèi)存中的一個(gè)變量傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用到變量的值的字節(jié)碼指令時(shí)將會(huì)執(zhí)行這個(gè)操作;
(6)assign,賦值,作用于工作內(nèi)存的變量,它把一個(gè)從執(zhí)行引擎接收到的變量賦值給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼指令時(shí)使用這個(gè)操作;
(7)store,存儲(chǔ),作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個(gè)變量的值傳遞到主內(nèi)存中,以便后續(xù)的write操作使用;
(8)write,寫(xiě)入,作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存得到的變量的值放入到主內(nèi)存的變量中;
如果要把一個(gè)變量從主內(nèi)存復(fù)制到工作內(nèi)存,那就要按順序地執(zhí)行read和load操作,同樣地,如果要把一個(gè)變量從工作內(nèi)存同步回主內(nèi)存,就要按順序地執(zhí)行store和write操作。注意,這里只說(shuō)明了要按順序,并沒(méi)有說(shuō)一定要連續(xù),也就是說(shuō)可以在read與load之間、store與write之間插入其它操作。比如,對(duì)主內(nèi)存中的變量a和b的訪問(wèn),可以按照以下順序執(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)存回寫(xiě)了但主內(nèi)存不接受的情況出現(xiàn);
(2)不允許一個(gè)線程丟棄它最近的assign操作,即變量在工作內(nèi)存變化了必須把該變化同步回主內(nèi)存;
(3)不允許一個(gè)線程無(wú)原因地(即未發(fā)生過(guò)assign操作)把一個(gè)變量從工作內(nèi)存同步回主內(nèi)存;
(4)一個(gè)新的變量必須在主內(nèi)存中誕生,不允許工作內(nèi)存中直接使用一個(gè)未被初始化(load或assign)過(guò)的變量,換句話說(shuō)就是對(duì)一個(gè)變量的use和store操作之前必須執(zhí)行過(guò)load和assign操作;
(5)一個(gè)變量同一時(shí)刻只允許一條線程對(duì)其進(jìn)行l(wèi)ock操作,但lock操作可以被同一個(gè)線程執(zhí)行多次,多次執(zhí)行l(wèi)ock后,只有執(zhí)行相同次數(shù)的unlock操作,變量才能被解鎖。
(6)如果對(duì)一個(gè)變量執(zhí)行l(wèi)ock操作,將會(huì)清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個(gè)變量前,需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值;
(7)如果一個(gè)變量沒(méi)有被lock操作鎖定,則不允許對(duì)其執(zhí)行unlock操作,也不允許unlock一個(gè)其它線程鎖定的變量;
(8)對(duì)一個(gè)變量執(zhí)行unlock操作之前,必須先把此變量同步回主內(nèi)存中,即執(zhí)行store和write操作;
注意,這里的lock和unlock是實(shí)現(xiàn)synchronized的基礎(chǔ),Java并沒(méi)有把lock和unlock操作直接開(kāi)放給用戶使用,但是卻提供了兩個(gè)更高層次的指令來(lái)隱式地使用這兩個(gè)操作,即moniterenter和moniterexit。
Java內(nèi)存模型就是為了解決多線程環(huán)境下共享變量的一致性問(wèn)題,那么一致性包含哪些內(nèi)容呢?
一致性主要包含三大特性:原子性、可見(jiàn)性、有序性,下面我們就來(lái)看看Java內(nèi)存模型是怎么實(shí)現(xiàn)這三大特性的。
(1)原子性
原子性是指一段操作一旦開(kāi)始就會(huì)一直運(yùn)行到底,中間不會(huì)被其它線程打斷,這段操作可以是一個(gè)操作,也可以是多個(gè)操作。
由Java內(nèi)存模型來(lái)直接保證的原子性操作包括read、load、user、assign、store、write這兩個(gè)操作,我們可以大致認(rèn)為基本類型變量的讀寫(xiě)是具備原子性的。
如果應(yīng)用需要一個(gè)更大范圍的原子性,Java內(nèi)存模型還提供了lock和unlock這兩個(gè)操作來(lái)滿足這種需求,盡管不能直接使用這兩個(gè)操作,但我們可以使用它們更具體的實(shí)現(xiàn)synchronized來(lái)實(shí)現(xiàn)。
因此,synchronized塊之間的操作也是原子性的。
(2)可見(jiàn)性
可見(jiàn)性是指當(dāng)一個(gè)線程修改了共享變量的值,其它線程能立即感知到這種變化。
Java內(nèi)存模型是通過(guò)在變更修改后同步回主內(nèi)存,在變量讀取前從主內(nèi)存刷新變量值來(lái)實(shí)現(xiàn)的,它是依賴主內(nèi)存的,無(wú)論是普通變量還是volatile變量都是如此。
普通變量與volatile變量的主要區(qū)別是是否會(huì)在修改之后立即同步回主內(nèi)存,以及是否在每次讀取前立即從主內(nèi)存刷新。因此我們可以說(shuō)volatile變量保證了多線程環(huán)境下變量的可見(jiàn)性,但普通變量不能保證這一點(diǎn)。
除了volatile之外,還有兩個(gè)關(guān)鍵字也可以保證可見(jiàn)性,它們是synchronized和final。
synchronized的可見(jiàn)性是由“對(duì)一個(gè)變量執(zhí)行unlock操作之前,必須先把此變量同步回主內(nèi)存中,即執(zhí)行store和write操作”這條規(guī)則獲取的。
final的可見(jiàn)性是指被final修飾的字段在構(gòu)造器中一旦被初始化完成,那么其它線程中就能看見(jiàn)這個(gè)final字段了。
(3)有序性
Java程序中天然的有序性可以總結(jié)為一句話:如果在本線程中觀察,所有的操作都是有序的;如果在另一個(gè)線程中觀察,所有的操作都是無(wú)序的。
前半句是指線程內(nèi)表現(xiàn)為串行的語(yǔ)義,后半句是指“指令重排序”現(xiàn)象和“工作內(nèi)存和主內(nèi)存同步延遲”現(xiàn)象。
Java中提供了volatile和synchronized兩個(gè)關(guān)鍵字來(lái)保證有序性。
volatile天然就具有有序性,因?yàn)槠浣怪嘏判颉?/p>
synchronized的有序性是由“一個(gè)變量同一時(shí)刻只允許一條線程對(duì)其進(jìn)行l(wèi)ock操作”這條規(guī)則獲取的。
如果Java內(nèi)存模型的有序性都只依靠volatile和synchronized來(lái)完成,那么有一些操作就會(huì)變得很啰嗦,但是我們?cè)诰帉?xiě)Java并發(fā)代碼時(shí)并沒(méi)有感受到,這是因?yàn)镴ava語(yǔ)言天然定義了一個(gè)“先行發(fā)生”原則,這個(gè)原則非常重要,依靠這個(gè)原則我們可以很容易地判斷在并發(fā)環(huán)境下兩個(gè)操作是否可能存在競(jìng)爭(zhēng)沖突問(wèn)題。
先行發(fā)生,是指操作A先行發(fā)生于操作B,那么操作A產(chǎn)生的影響能夠被操作B感知到,這種影響包括修改了共享內(nèi)存中變量的值、發(fā)送了消息、調(diào)用了方法等。
下面我們看看Java內(nèi)存模型定義的先行發(fā)生原則有哪些:
(1)程序次序原則
在一個(gè)線程內(nèi),按照程序書(shū)寫(xiě)的順序執(zhí)行,書(shū)寫(xiě)在前面的操作先行發(fā)生于書(shū)寫(xiě)在后面的操作,準(zhǔn)確地講是控制流順序而不是代碼順序,因?yàn)橐紤]分支、循環(huán)等情況。
(2)監(jiān)視器鎖定原則
一個(gè)unlock操作先行發(fā)生于后面對(duì)同一個(gè)鎖的lock操作。
(3)volatile原則
對(duì)一個(gè)volatile變量的寫(xiě)操作先行發(fā)生于后面對(duì)該變量的讀操作。
(4)線程啟動(dòng)原則
對(duì)線程的start()操作先行發(fā)生于線程內(nèi)的任何操作。
(5)線程終止原則
線程中的所有操作先行發(fā)生于檢測(cè)到線程終止,可以通過(guò)Thread.join()、Thread.isAlive()的返回值檢測(cè)線程是否已經(jīng)終止。
(6)線程中斷原則
對(duì)線程的interrupt()的調(diào)用先行發(fā)生于線程的代碼中檢測(cè)到中斷事件的發(fā)生,可以通過(guò)Thread.interrupted()方法檢測(cè)是否發(fā)生中斷。
(7)對(duì)象終結(jié)原則
一個(gè)對(duì)象的初始化完成(構(gòu)造方法執(zhí)行結(jié)束)先行發(fā)生于它的finalize()方法的開(kāi)始。
(8)傳遞性原則
如果操作A先行發(fā)生于操作B,操作B先行發(fā)生于操作C,那么操作A先行發(fā)生于操作C。
這里說(shuō)的“先行發(fā)生”與“時(shí)間上的先發(fā)生”沒(méi)有必然的關(guān)系。
比如,下面的代碼:
int a = 0;
// 操作A:線程1對(duì)進(jìn)行賦值操作
a = 1;
// 操作B:線程2獲取a的值
int b = a;
如果線程1在時(shí)間順序上先對(duì)a進(jìn)行賦值,然后線程2再獲取a的值,這能說(shuō)明操作A先行發(fā)生于操作B嗎?
顯然不能,因?yàn)榫€程2可能讀取的還是其工作內(nèi)存中的值,或者說(shuō)線程1并沒(méi)有把a(bǔ)的值刷新回主內(nèi)存呢,這時(shí)候線程2讀取到的值可能還是0。
所以,“時(shí)間上的先發(fā)生”不一定“先行發(fā)生”。
再看一個(gè)例子:
// 同一個(gè)線程中
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)槲覀冊(cè)谶@個(gè)線程中并不會(huì)感知到這點(diǎn)。
所以,“先行發(fā)生”不一定“時(shí)間上先發(fā)生”。
(1)硬件內(nèi)存架構(gòu)使得我們必須建立內(nèi)存模型來(lái)保證多線程環(huán)境下對(duì)共享內(nèi)存訪問(wèn)的正確性;
(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)存模型對(duì)原子性、可見(jiàn)性、有序性提供了一些實(shí)現(xiàn);
(5)先行發(fā)生的8大原則:程序次序原則、監(jiān)視器鎖定原則、volatile原則、線程啟動(dòng)原則、線程終止原則、線程中斷原則、對(duì)象終結(jié)原則、傳遞性原則;
(6)先行發(fā)生不等于時(shí)間上的先發(fā)生;
Java內(nèi)存模型是Java中很重要的概念,理解它非常有助于我們編寫(xiě)多線程代碼,理解多線程的本質(zhì),筆者這里整理了一些不錯(cuò)的資料提供給大家。
《深入理解Java虛擬機(jī)》
《Java并發(fā)編程的藝術(shù)》
《深入理解java內(nèi)存模型》
關(guān)注我的公眾號(hào)“彤哥讀源碼”回復(fù)“JMM”領(lǐng)取上面三本書(shū)籍。
歡迎關(guān)注我的公眾號(hào)“彤哥讀源碼”,查看更多源碼系列文章,與彤哥一起暢游源碼的海洋。
創(chuàng)新互聯(lián)www.cdcxhl.cn,專業(yè)提供香港、美國(guó)云服務(wù)器,動(dòng)態(tài)BGP最優(yōu)骨干路由自動(dòng)選擇,持續(xù)穩(wěn)定高效的網(wǎng)絡(luò)助力業(yè)務(wù)部署。公司持有工信部辦法的idc、isp許可證, 機(jī)房獨(dú)有T級(jí)流量清洗系統(tǒng)配攻擊溯源,準(zhǔn)確進(jìn)行流量調(diào)度,確保服務(wù)器高可用性。佳節(jié)活動(dòng)現(xiàn)已開(kāi)啟,新人活動(dòng)云服務(wù)器買(mǎi)多久送多久。