您好,登錄后才能下訂單哦!
Java內(nèi)存模型是在硬件內(nèi)存模型上的更高層的抽象,它屏蔽了各種硬件和操作系統(tǒng)訪問的差異性,保證了Java程序在各種平臺下對內(nèi)存的訪問都能達到一致的效果。
在正式講解Java的內(nèi)存模型之前,我們有必要先了解一下硬件層面的一些東西。
在現(xiàn)代計算機的硬件體系中,CPU的運算速度是非??斓?,遠遠高于它從存儲介質(zhì)讀取數(shù)據(jù)的速度,這里的存儲介質(zhì)有很多,比如磁盤、光盤、網(wǎng)卡、內(nèi)存等,這些存儲介質(zhì)有一個很明顯的特點——距離CPU越近的存儲介質(zhì)往往越小越貴越快,距離CPU越遠的存儲介質(zhì)往往越大越便宜越慢。
所以,在程序運行的過程中,CPU大部分時間都浪費在了磁盤IO、網(wǎng)絡(luò)通訊、數(shù)據(jù)庫訪問上,如果不想讓CPU在那里白白等待,我們就必須想辦法去把CPU的運算能力壓榨出來,否則就會造成很大的浪費,而讓CPU同時去處理多項任務(wù)則是最容易想到的,也是被證明非常有效的壓榨手段,這也就是我們常說的“并發(fā)執(zhí)行”。
但是,讓CPU并發(fā)地執(zhí)行多項任務(wù)并不是那么容易實現(xiàn)的事,因為所有的運算都不可能只依靠CPU的計算就能完成,往往還需要跟內(nèi)存進行交互,如讀取運算數(shù)據(jù)、存儲運算結(jié)果等。
前面我們也說過了,CPU與內(nèi)存的交互往往是很慢的,所以這就要求我們要想辦法在CPU和內(nèi)存之間建立一種連接,使它們達到一種平衡,讓運算能快速地進行,而這種連接就是我們常說的“高速緩存”。
高速緩存的速度是非常接近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ù)等基本原則。
而隨著CPU能力的不斷提升,一層緩存就無法滿足要求了,就逐漸衍生出了多級緩存。
按照數(shù)據(jù)讀取順序和CPU的緊密程度,CPU的緩存可以分為一級緩存(L1)、二級緩存(L2)、三級緩存(L3),每一級緩存存儲的數(shù)據(jù)都是下一級的一部分。
這三種緩存的技術(shù)難度和制作成本是相對遞減的,容量也是相對遞增的。
所以,在有了多級緩存后,程序的運行就變成了:
當CPU要讀取一個數(shù)據(jù)的時候,先從一級緩存中查找,如果沒找到再從二級緩存中查找,如果沒找到再從三級緩存中查找,如果沒找到再從主內(nèi)存中查找,然后再把找到的數(shù)據(jù)依次加載到多級緩存中,下次再使用相關(guān)的數(shù)據(jù)直接從緩存中查找即可。
而加載到緩存中的數(shù)據(jù)也不是說用到哪個就加載哪個,而是加載內(nèi)存中連續(xù)的數(shù)據(jù),一般來說是加載連續(xù)的64個字節(jié),因此,如果訪問一個 long 類型的數(shù)組時,當數(shù)組中的一個值被加載到緩存中時,另外 7 個元素也會被加載到緩存中,這就是“緩存行”的概念。
緩存行雖然能極大地提高程序運行的效率,但是在多線程對共享變量的訪問過程中又帶來了新的問題,也就是非常著名的“偽共享”。
關(guān)于偽共享的問題,我們這里就不展開講了,有興趣的可以看彤哥之前發(fā)布的【雜談 什么是偽共享(false sharing)?】章節(jié)的相關(guān)內(nèi)容。
除此之外,為了使CPU中的運算單元能夠充分地被利用,CPU可能會對輸入的代碼進行亂序執(zhí)行優(yōu)化,然后在計算之后再將亂序執(zhí)行的結(jié)果進行重組,保證該結(jié)果與順序執(zhí)行的結(jié)果一致,但并不保證程序中各個語句計算的先后順序與代碼的輸入順序一致,因此,如果一個計算任務(wù)依賴于另一個計算任務(wù)的結(jié)果,那么其順序性并不能靠代碼的先后順序來保證。
與CPU的亂序執(zhí)行優(yōu)化類似,java虛擬機的即時編譯器也有類似的指令重排序優(yōu)化。
為了解決上面提到的多個緩存讀寫一致性以及亂序排序優(yōu)化的問題,這就有了內(nèi)存模型,它定義了共享內(nèi)存系統(tǒng)中多線程讀寫操作行為的規(guī)范。
Java內(nèi)存模型(Java Memory Model,JMM)是在硬件內(nèi)存模型基礎(chǔ)上更高層的抽象,它屏蔽了各種硬件和操作系統(tǒng)對內(nèi)存訪問的差異性,從而實現(xiàn)讓Java程序在各種平臺下都能達到一致的并發(fā)效果。
Java內(nèi)存模型定義了程序中各個變量的訪問規(guī)則,即在虛擬機中將變量存儲到內(nèi)存和從內(nèi)存中取出這樣的底層細節(jié)。這里所說的變量包括實例字段、靜態(tài)字段,但不包括局部變量和方法參數(shù),因為它們是線程私有的,它們不會被共享,自然不存在競爭問題。
為了獲得更好的執(zhí)行效能,Java內(nèi)存模型并沒有限制執(zhí)行引擎使用處理器的特定寄存器或緩存來和主內(nèi)存進行交互,也沒有限制即時編譯器調(diào)整代碼的執(zhí)行順序等這類權(quán)利。
Java內(nèi)存模型規(guī)定了所有的變量都存儲在主內(nèi)存中,這里的主內(nèi)存跟介紹硬件時所用的名字一樣,兩者可以類比,但此處僅指虛擬機中內(nèi)存的一部分。
除了主內(nèi)存,每條線程還有自己的工作內(nèi)存,此處可與CPU的高速緩存進行類比。工作內(nèi)存中保存著該線程使用到的變量的主內(nèi)存副本的拷貝,線程對變量的操作都必須在工作內(nèi)存中進行,包括讀取和賦值等,而不能直接讀寫主內(nèi)存中的變量,不同的線程之間也無法直接訪問對方工作內(nèi)存中的變量,線程間變量值的傳遞必須通過主內(nèi)存來完成。
線程、工作內(nèi)存、主內(nèi)存三者的關(guān)系如下圖所示:
注意,這里所說的主內(nèi)存、工作內(nèi)存跟Java虛擬機內(nèi)存區(qū)域劃分中的堆、棧是不同層次的內(nèi)存劃分,如果兩者一定要勉強對應(yīng)起來,主內(nèi)存主要對應(yīng)于堆中對象的實例部分,而工作內(nèi)存主要對應(yīng)與虛擬機棧中的部分區(qū)域。
從更低層次來說,主內(nèi)存主要對應(yīng)于硬件內(nèi)存部分,工作內(nèi)存主要對應(yīng)于CPU的高速緩存和寄存器部分,但也不是絕對的,主內(nèi)存也可能存在于高速緩存和寄存器中,工作內(nèi)存也可能存在于硬件內(nèi)存中。
關(guān)于主內(nèi)存與工作內(nèi)存之間具體的交互協(xié)議,Java內(nèi)存模型定義了以下8種具體的操作來完成:
(1)lock,鎖定,作用于主內(nèi)存的變量,它把主內(nèi)存中的變量標識為一條線程獨占狀態(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í)行引擎,每當虛擬機遇到一個需要使用到變量的值的字節(jié)碼指令時將會執(zhí)行這個操作;
(6)assign,賦值,作用于工作內(nèi)存的變量,它把一個從執(zhí)行引擎接收到的變量賦值給工作內(nèi)存的變量,每當虛擬機遇到一個給變量賦值的字節(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操作之一單獨出現(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)一個變量同一時刻只允許一條線程對其進行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是實現(xiàn)synchronized的基礎(chǔ),Java并沒有把lock和unlock操作直接開放給用戶使用,但是卻提供了兩個更高層次的指令來隱式地使用這兩個操作,即moniterenter和moniterexit。
Java內(nèi)存模型就是為了解決多線程環(huán)境下共享變量的一致性問題,那么一致性包含哪些內(nèi)容呢?
一致性主要包含三大特性:原子性、可見性、有序性,下面我們就來看看Java內(nèi)存模型是怎么實現(xiàn)這三大特性的。
(1)原子性
原子性是指一段操作一旦開始就會一直運行到底,中間不會被其它線程打斷,這段操作可以是一個操作,也可以是多個操作。
由Java內(nèi)存模型來直接保證的原子性操作包括read、load、user、assign、store、write這兩個操作,我們可以大致認為基本類型變量的讀寫是具備原子性的。
如果應(yīng)用需要一個更大范圍的原子性,Java內(nèi)存模型還提供了lock和unlock這兩個操作來滿足這種需求,盡管不能直接使用這兩個操作,但我們可以使用它們更具體的實現(xiàn)synchronized來實現(xiàn)。
因此,synchronized塊之間的操作也是原子性的。
(2)可見性
可見性是指當一個線程修改了共享變量的值,其它線程能立即感知到這種變化。
Java內(nèi)存模型是通過在變更修改后同步回主內(nèi)存,在變量讀取前從主內(nèi)存刷新變量值來實現(xiàn)的,它是依賴主內(nèi)存的,無論是普通變量還是volatile變量都是如此。
普通變量與volatile變量的主要區(qū)別是是否會在修改之后立即同步回主內(nèi)存,以及是否在每次讀取前立即從主內(nèi)存刷新。因此我們可以說volatile變量保證了多線程環(huá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天然就具有有序性,因為其禁止重排序。
synchronized的有序性是由“一個變量同一時刻只允許一條線程對其進行l(wèi)ock操作”這條規(guī)則獲取的。
如果Java內(nèi)存模型的有序性都只依靠volatile和synchronized來完成,那么有一些操作就會變得很啰嗦,但是我們在編寫Java并發(fā)代碼時并沒有感受到,這是因為Java語言天然定義了一個“先行發(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ā)生于書寫在后面的操作,準確地講是控制流順序而不是代碼順序,因為要考慮分支、循環(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對進行賦值操作
a = 1;
// 操作B:線程2獲取a的值
int b = a;
如果線程1在時間順序上先對a進行賦值,然后線程2再獲取a的值,這能說明操作A先行發(fā)生于操作B嗎?
顯然不能,因為線程2可能讀取的還是其工作內(nèi)存中的值,或者說線程1并沒有把a的值刷新回主內(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ā)生原則的正確性,因為我們在這個線程中并不會感知到這點。
所以,“先行發(fā)生”不一定“時間上先發(fā)生”。
(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)存模型對原子性、可見性、有序性提供了一些實現(xiàn);
(5)先行發(fā)生的8大原則:程序次序原則、監(jiān)視器鎖定原則、volatile原則、線程啟動原則、線程終止原則、線程中斷原則、對象終結(jié)原則、傳遞性原則;
(6)先行發(fā)生不等于時間上的先發(fā)生;
Java內(nèi)存模型是Java中很重要的概念,理解它非常有助于我們編寫多線程代碼,理解多線程的本質(zhì),筆者這里整理了一些不錯的資料提供給大家。
《深入理解Java虛擬機》
《Java并發(fā)編程的藝術(shù)》
《深入理解java內(nèi)存模型》
關(guān)注我的公眾號“彤哥讀源碼”回復(fù)“JMM”領(lǐng)取上面三本書籍。
歡迎關(guān)注我的公眾號“彤哥讀源碼”,查看更多源碼系列文章,與彤哥一起暢游源碼的海洋。
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。