Java內(nèi)存區(qū)域與內(nèi)存模型詳解

這篇文章主要介紹“Java內(nèi)存區(qū)域與內(nèi)存模型詳解”,在日常操作中,相信很多人在Java內(nèi)存區(qū)域與內(nèi)存模型詳解問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Java內(nèi)存區(qū)域與內(nèi)存模型詳解”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!

網(wǎng)站的建設(shè)成都創(chuàng)新互聯(lián)公司專注網(wǎng)站定制,經(jīng)驗豐富,不做模板,主營網(wǎng)站定制開發(fā).小程序定制開發(fā),H5頁面制作!給你煥然一新的設(shè)計體驗!已為護欄打樁機等企業(yè)提供專業(yè)服務(wù)。

簡介

首先介紹兩個名詞:1)可見性:一個線程對共享變量值的修改,能夠及時地被其他線程看到。2)共享變量:如果一個變量在多個線程的工作內(nèi)存中都存在副本,那么這個變量就是這幾個線程的共享變量

Java線程之間的通信對程序員完全透明,在并發(fā)編程中,需要處理兩個關(guān)鍵問題:線程之間如何通信及線程之間如何同步。

通信:通信是指線程之間以何種機制來交換信息。在命令式編程中,線程之間的通信機制有兩種:共享內(nèi)存和消息傳遞。在共享內(nèi)存的并發(fā)模型里,線程之間共享程序的公共狀態(tài),通過寫-讀內(nèi)存中的公共狀態(tài)來進行隱式通信。在消息傳遞的并發(fā)模型里,線程之間沒有公共狀態(tài),線程之間必須通過發(fā)送消息來進行顯示通信。

同步:同步是指程序中用于控制不同線程間操作發(fā)生相對順序的機制。在共享內(nèi)存并發(fā)模型里,同步是顯示進行的,程序員必須顯示指定某個方法或某段代碼需要在線程之間互斥執(zhí)行。在消息傳遞的并發(fā)模型里,由于消息的發(fā)送必須在消息的接收之前,因此同步是隱式進行的。

Java并發(fā)采用的是共享內(nèi)存模型。

一、Java內(nèi)存區(qū)域(JVM內(nèi)存區(qū)域)

Java內(nèi)存區(qū)域與內(nèi)存模型詳解

Java虛擬機在運行程序時會把其自動管理的內(nèi)存劃分為以上幾個區(qū)域,每個區(qū)域都有的用途以及創(chuàng)建銷毀的時機,其中藍色部分代表的是所有線程共享的數(shù)據(jù)區(qū)域,而綠色部分代表的是每個線程的私有數(shù)據(jù)區(qū)域。

  • 方法區(qū)(Method Area):

    方法區(qū)屬于線程共享的內(nèi)存區(qū)域,又稱Non-Heap(非堆),主要用于存儲已被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù),根據(jù)Java 虛擬機規(guī)范的規(guī)定,當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時,將拋出OutOfMemoryError 異常。值得注意的是在方法區(qū)中存在一個叫運行時常量池(Runtime Constant Pool)的區(qū)域,它主要用于存放編譯器生成的各種字面量和符號引用,這些內(nèi)容將在類加載后存放到運行時常量池中,以便后續(xù)使用。

  • JVM堆(Java Heap):

    Java 堆也是屬于線程共享的內(nèi)存區(qū)域,它在虛擬機啟動時創(chuàng)建,是Java 虛擬機所管理的內(nèi)存中最大的一塊,主要用于存放對象實例,幾乎所有的對象實例都在這里分配內(nèi)存,注意Java 堆是垃圾收集器管理的主要區(qū)域,因此很多時候也被稱做GC 堆,如果在堆中沒有內(nèi)存完成實例分配,并且堆也無法再擴展時,將會拋出OutOfMemoryError 異常。

  • 程序計數(shù)器(Program Counter Register):

    屬于線程私有的數(shù)據(jù)區(qū)域,是一小塊內(nèi)存空間,主要代表當(dāng)前線程所執(zhí)行的字節(jié)碼行號指示器。字節(jié)碼解釋器工作時,通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個計數(shù)器來完成。

  • 虛擬機棧(Java Virtual Machine Stacks):

    屬于線程私有的數(shù)據(jù)區(qū)域,與線程同時創(chuàng)建,總數(shù)與線程關(guān)聯(lián),代表Java方法執(zhí)行的內(nèi)存模型。棧中只保存基礎(chǔ)數(shù)據(jù)類型和自定義對象的引用(不是對象),對象都存放在堆區(qū)中。每個方法執(zhí)行時都會創(chuàng)建一個棧楨來存儲方法的的變量表、操作數(shù)棧、動態(tài)鏈接方法、返回值、返回地址等信息。每個方法從調(diào)用直結(jié)束就對于一個棧楨在虛擬機棧中的入棧和出棧過程,如下(圖有誤,應(yīng)該為棧楨):

    Java內(nèi)存區(qū)域與內(nèi)存模型詳解

  • 本地方法棧(Native Method Stacks):

    本地方法棧屬于線程私有的數(shù)據(jù)區(qū)域,這部分主要與虛擬機用到的 Native 方法相關(guān),一般情況下,我們無需關(guān)心此區(qū)域。

二、Java內(nèi)存模型

Java內(nèi)存模型(即Java Memory Model,簡稱JMM)本身是一種抽象的概念,并不真實存在。Java線程之間的通信由JMM控制,JMM決定一個線程對共享變量的寫入何時對另一個線程可見。從抽象的角度來看,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系。



由于JVM運行程序的實體是線程,而每個線程創(chuàng)建時JVM都會為其創(chuàng)建一個工作內(nèi)存(有些地方稱為棧空間),用于存儲線程私有的數(shù)據(jù),而Java內(nèi)存模型中規(guī)定所有變量都存儲在主內(nèi)存,主內(nèi)存是共享內(nèi)存區(qū)域,所有線程都可以訪問,但線程對變量的操作(讀取賦值等)必須在工作內(nèi)存中進行。


首先要將變量從主內(nèi)存拷貝的自己的工作內(nèi)存空間,然后對變量進行操作,操作完成后再將變量寫回主內(nèi)存,不能直接操作主內(nèi)存中的變量,工作內(nèi)存中存儲著主內(nèi)存中的變量副本拷貝,前面說過,工作內(nèi)存是每個線程的私有數(shù)據(jù)區(qū)域,因此不同的線程間無法訪問對方的工作內(nèi)存,線程間的通信(傳值)必須通過主內(nèi)存來完成,其簡要訪問過程如下圖

Java內(nèi)存區(qū)域與內(nèi)存模型詳解

圖3

需要注意的是,JMM與Java內(nèi)存區(qū)域的劃分是不同的概念層次,更恰當(dāng)說JMM描述的是一組規(guī)則,通過這組規(guī)則控制程序中各個變量在共享數(shù)據(jù)區(qū)域和私有數(shù)據(jù)區(qū)域的訪問方式,JMM是圍繞原子性,有序性、可見性展開的(稍后會分析)。

JMM與Java內(nèi)存區(qū)域唯一相似點,都存在共享數(shù)據(jù)區(qū)域和私有數(shù)據(jù)區(qū)域,在JMM中主內(nèi)存屬于共享數(shù)據(jù)區(qū)域,從某個程度上講應(yīng)該包括了堆和方法區(qū),而工作內(nèi)存數(shù)據(jù)線程私有數(shù)據(jù)區(qū)域,從某個程度上講則應(yīng)該包括程序計數(shù)器、虛擬機棧以及本地方法棧。或許在某些地方,我們可能會看見主內(nèi)存被描述為堆內(nèi)存,工作內(nèi)存被稱為線程棧,實際上他們表達的都是同一個含義。關(guān)于JMM中的主內(nèi)存和工作內(nèi)存說明如下

  • 主內(nèi)存

    主要存儲的是Java實例對象以及線程之間的共享變量,所有線程創(chuàng)建的實例對象都存放在主內(nèi)存中,不管該實例對象是成員變量還是方法中的本地變量(也稱局部變量),當(dāng)然也包括了共享的類信息、常量、靜態(tài)變量。由于是共享數(shù)據(jù)區(qū)域,多條線程對同一個變量進行訪問可能會發(fā)現(xiàn)線程安全問題。

  • 工作內(nèi)存

    有的書籍中也稱為本地內(nèi)存,主要存儲當(dāng)前方法的所有本地變量信息(工作內(nèi)存中存儲著主內(nèi)存中的變量副本拷貝),每個線程只能訪問自己的工作內(nèi)存,即線程中的本地變量對其它線程是不可見的,就算是兩個線程執(zhí)行的是同一段代碼,它們也會各自在自己的工作內(nèi)存中創(chuàng)建屬于當(dāng)前線程的本地變量,當(dāng)然也包括了字節(jié)碼行號指示器、相關(guān)Native方法的信息。

    注意由于工作內(nèi)存是每個線程的私有數(shù)據(jù),線程間無法相互訪問工作內(nèi)存,因此存儲在工作內(nèi)存的數(shù)據(jù)不存在線程安全問題。注意,工作內(nèi)存是JMM的一個抽象概念,并不真實存在。

弄清楚主內(nèi)存和工作內(nèi)存后,接了解一下主內(nèi)存與工作內(nèi)存的數(shù)據(jù)存儲類型以及操作方式,根據(jù)虛擬機規(guī)范,對于一個實例對象中的成員方法而言,如果方法中包含本地變量是基本數(shù)據(jù)類型(boolean,byte,short,char,int,long,float,double),將直接存儲在工作內(nèi)存的幀棧結(jié)構(gòu)中,但倘若本地變量是引用類型,那么該變量的引用會存儲在功能內(nèi)存的幀棧中,而對象實例將存儲在主內(nèi)存(共享數(shù)據(jù)區(qū)域,堆)中。




但對于實例對象的成員變量,不管它是基本數(shù)據(jù)類型或者包裝類型(Integer、Double等)還是引用類型,都會被存儲到堆區(qū)。至于static變量以及類本身相關(guān)信息將會存儲在主內(nèi)存中。需要注意的是,在主內(nèi)存中的實例對象可以被多線程共享,倘若兩個線程同時調(diào)用了同一個對象的同一個方法,那么兩條線程會將要操作的數(shù)據(jù)拷貝一份到自己的工作內(nèi)存中,執(zhí)行完成操作后才刷新到主內(nèi)存,簡單示意圖如下所示:

Java內(nèi)存區(qū)域與內(nèi)存模型詳解

圖4

從圖3來看,如果線程A與線程B之間要通信的話,必須經(jīng)歷下面兩個步驟:

1)線程A把本地內(nèi)存A中更新過的共享變量刷新到主內(nèi)存中去

2)線程B到主內(nèi)存中去讀取線程A之前已更新過的共享變量

從以上兩個步驟來看,共享內(nèi)存模型完成了“隱式通信”的過程。

JMM也主要是通過控制主內(nèi)存與每個線程的工作內(nèi)存之間的交互,來為Java程序員提供內(nèi)存可見性的保證。

三、as-if-serial語義、happens-before原則

3.1 as-if-serial語義

重排序是指編譯器和處理器為了優(yōu)化程序性能而對指令序列進行重新排序的一種手段。as-if-serial語義的意思是:不管怎么重排序(編譯器和處理器為了提高并行度),(單線程)程序的執(zhí)行結(jié)果不能被改變。編譯器、runtime和處理器都必須遵守as-if-serial語義。為了遵守as-if-serial語義,編譯器和處理器不會對存在數(shù)據(jù)依賴關(guān)系的操作做重排序,因為這種重排序會改變執(zhí)行結(jié)果。

但是,如果操作之間不存在數(shù)據(jù)依賴關(guān)系,這些操作就可能被編譯器和處理器重排序。

3.2 happens-before原則

happens-before是JMM最核心的概念。對應(yīng)Java程序來說,理解happens-before是理解JMM的關(guān)鍵。

設(shè)計JMM時,需要考慮兩個關(guān)鍵因素:

    • 程序員對內(nèi)存模型的使用。程序員希望內(nèi)存模型易于理解、易于編程。程序員希望基于一個強內(nèi)存模型來編寫代碼。

    • 編譯器和處理器對內(nèi)存模型的實現(xiàn)。編譯器和處理器希望內(nèi)存模型對它們的束縛越少越好,這樣它們就可以做盡可能多的優(yōu)化來提高性能。編譯器和處理器希望實現(xiàn)弱內(nèi)存模型。

但以上兩點相互矛盾,所以JSR-133專家組在設(shè)計JMM時的核心膜表就是找到一個好的平衡點:一方面,為程序員提高足夠強的內(nèi)存可見性保證;另一方面,對編譯器和處理器的限制盡可能地放松。

另外還要一個特別有意思的事情就是關(guān)于重排序問題,更簡單的說,重排序可以分為兩類:1)會改變程序執(zhí)行結(jié)果的重排序。 2) 不會改變程序執(zhí)行結(jié)果的重排序。

JMM對這兩種不同性質(zhì)的重排序,采取了不同的策略,如下:

  • 對于會改變程序執(zhí)行結(jié)果的重排序,JMM要求編譯器和處理器必須禁止這種重排序。

  • 對于不會改變程序執(zhí)行結(jié)果的重排序,JMM對編譯器和處理器不做要求(JMM允許這種 重排序)

JMM的設(shè)計圖為:

Java內(nèi)存區(qū)域與內(nèi)存模型詳解

JMM設(shè)計示意圖 從圖可以看出:

  • JMM向程序員提供的happens-before規(guī)則能滿足程序員的需求。JMM的happens-before規(guī)則不但簡單易懂,而且也向程序員提供了足夠強的內(nèi)存可見性保證(有些內(nèi)存可見性保證其實并不一定真實存在,比如上面的A happens-before B)。

  • JMM對編譯器和處理器的束縛已經(jīng)盡可能少。從上面的分析可以看出,JMM其實是在遵循一個基本原則:只要不改變程序的執(zhí)行結(jié)果(指的是單線程程序和正確同步的多線程程序),編譯器和處理器怎么優(yōu)化都行。例如,如果編譯器經(jīng)過細致的分析后,認定一個鎖只會被單個線程訪問,那么這個鎖可以被消除。再如,如果編譯器經(jīng)過細致的分析后,認定一個volatile變量只會被單個線程訪問,那么編譯器可以把這個volatile變量當(dāng)作一個普通變量來對待。這些優(yōu)化既不會改變程序的執(zhí)行結(jié)果,又能提高程序的執(zhí)行效率。

3.3 happens-before定義

happens-before的概念最初由Leslie Lamport在其一篇影響深遠的論文(《Time,Clocks and the Ordering of Events in a Distributed System》)中提出。JSR-133使用happens-before的概念來指定兩個操作之間的執(zhí)行順序。由于這兩個操作可以在一個線程之內(nèi),也可以是在不同線程之間。因此,JMM可以通過happens-before關(guān)系向程序員提供跨線程的內(nèi)存可見性保證(如果A線程的寫操作a與B線程的讀操作b之間存在happens-before關(guān)系,盡管a操作和b操作在不同的線程中執(zhí)行,但JMM向程序員保證a操作將對b操作可見)。具體的定義為:

1)如果一個操作happens-before另一個操作,那么第一個操作的執(zhí)行結(jié)果將對第二個操作可見,而且第一個操作的執(zhí)行順序排在第二個操作之前。

2)兩個操作之間存在happens-before關(guān)系,并不意味著Java平臺的具體實現(xiàn)必須要按照happens-before關(guān)系指定的順序來執(zhí)行。如果重排序之后的執(zhí)行結(jié)果,與按happens-before關(guān)系來執(zhí)行的結(jié)果一致,那么這種重排序并不非法(也就是說,JMM允許這種重排序)。

上面的1)是JMM對程序員的承諾。從程序員的角度來說,可以這樣理解happens-before關(guān)系:如果A happens-before B,那么Java內(nèi)存模型將向程序員保證——A操作的結(jié)果將對B可見,且A的執(zhí)行順序排在B之前。注意,這只是Java內(nèi)存模型向程序員做出的保證!

上面的2)是JMM對編譯器和處理器重排序的約束原則。正如前面所言,JMM其實是在遵循一個基本原則:只要不改變程序的執(zhí)行結(jié)果(指的是單線程程序和正確同步的多線程程序),編譯器和處理器怎么優(yōu)化都行。JMM這么做的原因是:程序員對于這兩個操作是否真的被重排序并不關(guān)心,程序員關(guān)心的是程序執(zhí)行時的語義不能被改變(即執(zhí)行結(jié)果不能被改變)。因此,happens-before關(guān)系本質(zhì)上和as-if-serial語義是一回事。

3.3 happens-before對比as-if-serial

  • as-if-serial語義保證單線程內(nèi)程序的執(zhí)行結(jié)果不被改變,happens-before關(guān)系保證正確同步的多線程程序的執(zhí)行結(jié)果不被改變。

  • as-if-serial語義給編寫單線程程序的程序員創(chuàng)造了一個幻境:單線程程序是按程序的順序來執(zhí)行的。happens-before關(guān)系給編寫正確同步的多線程程序的程序員創(chuàng)造了一個幻境:正確同步的多線程程序是按happens-before指定的順序來執(zhí)行的。

  • as-if-serial語義和happens-before這么做的目的,都是為了在不改變程序執(zhí)行結(jié)果的前提下,盡可能地提高程序執(zhí)行的并行度。

3.4 happens-before具體規(guī)則

  • 程序順序規(guī)則:一個線程中的每個操作,happens-before于該線程中的任意后續(xù)操作。

  • 監(jiān)視器鎖規(guī)則:對一個鎖的解鎖,happens-before于隨后對這個鎖的加鎖。

  • volatile變量規(guī)則:對一個volatile域的寫,happens-before于任意后續(xù)對這個volatile域的讀。

  • 傳遞性:如果A happens-before B,且B happens-before C,那么A happens-before C。

  • start()規(guī)則:如果線程A執(zhí)行操作ThreadB.start()(啟動線程B),那么A線程的ThreadB.start()操作happens-before于線程B中的任意操作。

  • join()規(guī)則:如果線程A執(zhí)行操作ThreadB.join()并成功返回,那么線程B中的任意操作happens-before于線程A從ThreadB.join()操作成功返回。

3.5 happens-before與JMM的關(guān)系圖

Java內(nèi)存區(qū)域與內(nèi)存模型詳解

一個happens-before規(guī)則對應(yīng)于一個或多個編譯器和處理器重排序規(guī)則。對于Java程序員來說,happens-before規(guī)則簡單易懂,它避免Java程序員為了理解JMM提供的內(nèi)存可見性保證而去學(xué)習(xí)復(fù)雜的重排序規(guī)則以及這些規(guī)則的具體實現(xiàn)方法

四、volatile、鎖的內(nèi)存語義

4.1 volatile的內(nèi)存語義

當(dāng)聲明共享變量為volatile后,對這個變量的讀/寫會很特別。一個volatile變量的單個讀/寫操作,與一個普通變量的讀/寫操作都是使用同一個鎖來同步,它們之間的執(zhí)行效果相同。

鎖的happens-before規(guī)則保證釋放鎖和獲取鎖的兩個線程之間的內(nèi)存可見性,這意味著對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最后的寫入。

鎖的語義決定了臨界區(qū)代碼的執(zhí)行具有原子性。這意味著,即使是64位的long型和double型變量,只要是volatile變量,對該變量的讀/寫就具有原子性。如果是多個volatile操作或類似于volatile++這種復(fù)合操作,這些操作整體上不具有原子性。

簡而言之,一旦一個共享變量(類的成員變量、類的靜態(tài)成員變量)被volatile修飾之后,那么就具備了兩層語義:

1)保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。

2)禁止進行指令重排序。

  • 可見性。對一個volatiole變量的讀,總是能看到(任意線程)對這個volatile變量最后的寫入。

  • 有序性。volatile關(guān)鍵字能禁止指令重排序,所以volatile能在一定程度上保證有序性。

    volatile關(guān)鍵字禁止指令重排序有兩層意思:

    1)當(dāng)程序執(zhí)行到volatile變量的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經(jīng)進行,且結(jié)果已經(jīng)對后面的操作可見;在其后面的操作肯定還沒有進行;

    2)在進行指令優(yōu)化時,不能將在對volatile變量訪問的語句放在其后面執(zhí)行,也不能把volatile變量后面的語句放到其前面執(zhí)行。

    可能上面說的比較繞,舉個簡單的例子:

//x、y為非volatile變量 //flag為volatile變量
 x = 2;        //語句1
y = 0;        //語句2
flag = true;  //語句3
x = 4;         //語句4
y = -1;       //語句5

由于flag變量為volatile變量,那么在進行指令重排序的過程的時候,不會將語句3放到語句1、語句2前面,也不會講語句3放到語句4、語句5后面。但是要注意語句1和語句2的順序、語句4和語句5的順序是不作任何保證的。

  • 原子性。對任意單個volatile變量的讀、寫具有原子性,但類似于volatile++這種復(fù)合操作不具有原子性。

volatile寫的內(nèi)存語義:當(dāng)寫一個volatile變量時,JMM會把該線程對應(yīng)的本地內(nèi)存中的共享變量值刷新到主內(nèi)存。

volatile讀的內(nèi)存語義:當(dāng)讀一個volatile變量時,JMM會把該線程對應(yīng)的本地內(nèi)存置位無效。線程接下來將從主內(nèi)存中讀取共享變量。(強制從主內(nèi)存讀取共享變量,把本地內(nèi)存與主內(nèi)存的共享變量的值變成一致)。

volatile寫和讀的內(nèi)存語義總結(jié)總結(jié):

  • 線程A寫一個volatile變量,實質(zhì)上是線程A向接下來將要讀這個volatile變量的某個線程發(fā)出了(其對變量所做修改的)消息。

  • 線程B讀一個volatile變量,實質(zhì)上是線程B接收了之前某個線程發(fā)出的消息。

  • 線程A寫一個volatile變量,隨后線程B讀這個volatile變量,這個過程實質(zhì)上是線程A通過主內(nèi)存向線程B發(fā)送消息。(隱式通信)

4.2 volatile內(nèi)存語義的實現(xiàn)

前面提到過編譯器重排序和處理器重排序。為了實現(xiàn)volatile內(nèi)存語義,JMM分別限制了這兩種類型的重排序類型。

  • 當(dāng)?shù)诙€操作是volatile寫時,不管第一個操作是什么,都不能重排序。這個規(guī)則確保volatile寫之前的操作不會被編譯器重排序到volatile寫之后。

  • 當(dāng)?shù)谝粋€操作是volatile讀時,不管第二個操作是什么,都不能重排序。這個規(guī)則確保volatile讀之后的操作不會被編譯器重排序到volatile讀之前。

  • 當(dāng)?shù)谝粋€操作是volatile寫時,第二個操作是volatile讀時,不能重排序。

為了實現(xiàn)volatile的內(nèi)存語義,編譯器在生成字節(jié)碼時,會在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序。對于編譯器來說,發(fā)現(xiàn)一個最優(yōu)布置來最小化插入屏障的總數(shù)幾乎不可能。為此,JMM采取保守策略。下面是基于保守策略的JMM內(nèi)存屏障插入策略:

  • 在每個volatile寫操作的前面插入一個StoreStore屏障。

  • 在每個volatile寫操作的后面插入一個StoreLoad屏障。

  • 在每個volatile讀操作的后面插入一個LoadLoad屏障。

  • 在每個volatile讀操作的后面插入一個LoadStore屏障。

上述內(nèi)存屏障插入策略非常保守,但它可以保證在任意處理器平臺,任意的程序中都能得到正確的volatile內(nèi)存語義。

下面是保守策略下,volatile寫插入內(nèi)存屏障后生成的指令序列示意圖:

Java內(nèi)存區(qū)域與內(nèi)存模型詳解

上圖中的StoreStore屏障可以保證在volatile寫之前,其前面的所有普通寫操作已經(jīng)對任意處理器可見了。這是因為StoreStore屏障將保障上面所有的普通寫在volatile寫之前刷新到主內(nèi)存。

這里比較有意思的是volatile寫后面的StoreLoad屏障。這個屏障的作用是避免volatile寫與后面可能有的volatile讀/寫操作重排序。因為編譯器常常無法準(zhǔn)確判斷在一個volatile寫的后面,是否需要插入一個StoreLoad屏障(比如,一個volatile寫之后方法立即return)。為了保證能正確實現(xiàn)volatile的內(nèi)存語義,JMM在這里采取了保守策略:在每個volatile寫的后面或在每個volatile讀的前面插入一個StoreLoad屏障。從整體執(zhí)行效率的角度考慮,JMM選擇了在每個volatile寫的后面插入一個StoreLoad屏障。因為volatile寫-讀內(nèi)存語義的常見使用模式是:一個寫線程寫volatile變量,多個讀線程讀同一個volatile變量。當(dāng)讀線程的數(shù)量大大超過寫線程時,選擇在volatile寫之后插入StoreLoad屏障將帶來可觀的執(zhí)行效率的提升。從這里我們可以看到JMM在實現(xiàn)上的一個特點:首先確保正確性,然后再去追求執(zhí)行效率。下面是在保守策略下,volatile讀插入內(nèi)存屏障后生成的指令序列示意圖:

Java內(nèi)存區(qū)域與內(nèi)存模型詳解

上圖中的LoadLoad屏障用來禁止處理器把上面的volatile讀與下面的普通讀重排序。LoadStore屏障用來禁止處理器把上面的volatile讀與下面的普通寫重排序。

上述volatile寫和volatile讀的內(nèi)存屏障插入策略非常保守。在實際執(zhí)行時,只要不改變volatile寫-讀的內(nèi)存語義,編譯器可以根據(jù)具體情況省略不必要的屏障。下面我們通過具體的示例代碼來說明:

class VolatileBarrierExample { int a; volatile int v1 = 1; volatile int v2 = 2; void readAndWrite() { int i = v1;      //第一個volatile讀
    int j = v2;      // 第二個volatile讀
    a = i + j;      //普通寫
    v1 = i + 1;     // 第一個volatile寫
    v2 = j * 2;     //第二個 volatile寫
 }
  … //其他方法
}

針對readAndWrite()方法,編譯器在生成字節(jié)碼時可以做如下的優(yōu)化:

Java內(nèi)存區(qū)域與內(nèi)存模型詳解

注意,最后的StoreLoad屏障不能省略。因為第二個volatile寫之后,方法立即return。此時編譯器可能無法準(zhǔn)確斷定后面是否會有volatile讀或?qū)?,為了安全起見,編譯器常常會在這里插入一個StoreLoad屏障。

上面的優(yōu)化是針對任意處理器平臺,由于不同的處理器有不同“松緊度”的處理器內(nèi)存模型,內(nèi)存屏障的插入還可以根據(jù)具體的處理器內(nèi)存模型繼續(xù)優(yōu)化。以x86處理器為例,上圖中除最后的StoreLoad屏障外,其它的屏障都會被省略。

為了提供一種比鎖更輕量級的線程之間通信的機制,JSR-133專家組決定增強volatile的內(nèi)存語義:嚴(yán)格限制編譯器和處理器對volatile變量與普通變量的重排序,確保volatile的寫-讀和鎖的釋放-獲取具有相同的內(nèi)存語義。

由于volatile僅僅保證對單個volatile變量的讀/寫具有原子性,而鎖的互斥執(zhí)行的特性可以確保對整個臨界區(qū)代碼的執(zhí)行具有原子性。在功能上,鎖比volatile更強大;在可伸縮性和執(zhí)行性能上,volatile更有優(yōu)勢。

當(dāng)一個變量被定義為volatile之后,就可以保證此變量對所有線程的可見性,即當(dāng)一個線程修改了此變量的值的時候,變量新的值對于其他線程來說是可以立即得知的。可以理解成:對volatile變量所有的寫操作都能立刻被其他線程得知。但是這并不代表基于volatile變量的運算在并發(fā)下是安全的,因為volatile只能保證內(nèi)存可見性,卻沒有保證對變量操作的原子性。比如下面的代碼:

/ * 
 * 發(fā)起20個線程,每個線程對race變量進行10000次自增操作,如果代碼能夠正確并發(fā),
 * 則最終race的結(jié)果應(yīng)為200000,但實際的運行結(jié)果卻小于200000。
 * 
 * @author Colin Wang */
public class Test { public static volatile int race = 0; public static void increase() {
        race++;
    } private static final int THREADS_COUNT = 20; public static void main(String[] args) {
        Thread[] threads = new Thread[THREADS_COUNT]; for (int i = 0; i < THREADS_COUNT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override public void run() { for (int i = 0; i < 10000; i++) {
                        increase();
                    }
                }
            });
            threads[i].start();
        } while (Thread.activeCount() > 1)
            Thread.yield();
        System.out.println(race);
    }
}

按道理來說結(jié)果是10000,但是運行下很可能是個小于10000的值。有人可能會說volatile不是保證了可見性啊,一個線程對race的修改,另外一個線程應(yīng)該立刻看到??!可是這里的操作race++是個復(fù)合操作啊,包括讀取race的值,對其自增,然后再寫回主存。

假設(shè)線程A,讀取了race的值為10,這時候被阻塞了,因為沒有對變量進行修改,觸發(fā)不了volatile規(guī)則。

線程B此時也讀讀race的值,主存里race的值依舊為10,做自增,然后立刻就被寫回主存了,為11。

此時又輪到線程A執(zhí)行,由于工作內(nèi)存里保存的是10,所以繼續(xù)做自增,再寫回主存,11又被寫了一遍。所以雖然兩個線程執(zhí)行了兩次increase(),結(jié)果卻只加了一次。

有人說,volatile不是會使緩存行無效的嗎?但是這里線程A讀取到線程B也進行操作之前,并沒有修改inc值,所以線程B讀取的時候,還是讀的10。

又有人說,線程B將11寫回主存,不會把線程A的緩存行設(shè)為無效嗎?但是線程A的讀取操作已經(jīng)做過了啊,只有在做讀取操作時,發(fā)現(xiàn)自己緩存行無效,才會去讀主存的值,所以這里線程A只能繼續(xù)做自增了。

綜上所述,在這種復(fù)合操作的情景下,原子性的功能是維持不了了。但是volatile在上面那種設(shè)置flag值的例子里,由于對flag的讀/寫操作都是單步的,所以還是能保證原子性的。

要想保證原子性,只能借助于synchronized,Lock以及并發(fā)包下的atomic的原子操作類了,即對基本數(shù)據(jù)類型的 自增(加1操作),自減(減1操作)、以及加法操作(加一個數(shù)),減法操作(減一個數(shù))進行了封裝,保證這些操作是原子性操作。

Java 理論與實踐: 正確使用 Volatile 變量 總結(jié)了volatile關(guān)鍵的使用場景,

只能在有限的一些情形下使用 volatile 變量替代鎖。要使 volatile 變量提供理想的線程安全,必須同時滿足下面兩個條件:

  • 對變量的寫操作不依賴于當(dāng)前值。

  • 該變量沒有包含在具有其他變量的不變式中。

實際上,這些條件表明,可以被寫入 volatile 變量的這些有效值獨立于任何程序的狀態(tài),包括變量的當(dāng)前狀態(tài)。

第一個條件的限制使 volatile 變量不能用作線程安全計數(shù)器。雖然增量操作(x++)看上去類似一個單獨操作,實際上它是一個由讀?。薷模瓕懭氩僮餍蛄薪M成的組合操作,必須以原子方式執(zhí)行,而 volatile 不能提供必須的原子特性。實現(xiàn)正確的操作需要使x 的值在操作期間保持不變,而 volatile 變量無法實現(xiàn)這點。(然而,如果將值調(diào)整為只從單個線程寫入,那么可以忽略第一個條件。)

volatile一個使用場景是狀態(tài)位;還有只有一個線程寫,其余線程讀的場景

4.3 鎖的內(nèi)存語義

鎖可以讓臨界區(qū)互斥執(zhí)行。鎖的釋放-獲取的內(nèi)存語義與volatile變量寫-讀的內(nèi)存語義很像。

當(dāng)線程釋放鎖時,JMM會把該線程對應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存中。

當(dāng)線程獲取鎖時,JMM會把該線程對應(yīng)的本地內(nèi)存置位無效,從而使得被監(jiān)視器保護的臨界區(qū)代碼必須從主內(nèi)存中讀取共享變量。

不難發(fā)現(xiàn):鎖釋放與volatile寫有相同的內(nèi)存語音;鎖獲取與volatile讀有相同的內(nèi)存語義。

下面對鎖釋放和鎖獲取的內(nèi)存語義做個總結(jié)。

  • 線程A釋放一個鎖,實質(zhì)上是線程A向接下來將要獲取這個鎖的某個線程發(fā)出了(線程A對共享變量所做修改的)消息。

  • 線程B獲取一個鎖,實質(zhì)上是線程B接收了之前某個線程發(fā)出的(在釋放這個鎖之前對共享變量所做修改)的消息。

  • 線程A釋放鎖,隨后線程B獲取這個鎖,這個過程實質(zhì)上是線程A通過主內(nèi)存向線程B發(fā)送消息。

4.4 final域的內(nèi)存語義

與前面介紹的鎖和volatile想比,對final域的讀和寫更像是普通的變量訪問。

對于final域,編譯器和處理器要遵循兩個重排序規(guī)則:

1.在構(gòu)造函數(shù)內(nèi)對一個final域的寫入,與隨后把這個被構(gòu)造對象的引用賦值給一個引用變量,這兩個操作之間不能重排序。

2.初次讀一個包含final域的對象的應(yīng)用,與隨后初次讀這個final域,這兩個操作之間不能重排序

下面通過一個示例來分別說明這兩個規(guī)則:

public class FinalTest { int i;//普通變量
    final int j; static FinalExample obj; public FinalExample(){
        i = 1;
        j = 2;
    } public static void writer(){
        obj = new FinalExample();
    } public static void reader(){
        FinalExample object = obj;//讀對象引用
        int a = object.i; int b = object.j;
    }
}

這里假設(shè)一個線程A執(zhí)行writer()方法,隨后另一個線程B執(zhí)行reader()方法。下面我們通過這兩個線程的交互來說明這兩個規(guī)則。

寫final域的重排序規(guī)則禁止把final域的寫重排序到構(gòu)造函數(shù)之外。這個規(guī)則的實現(xiàn)包含下面兩個方面。

1)JMM禁止編譯器把final域的寫重排序到構(gòu)造函數(shù)之外。

2)編譯器會在final域的寫之后,構(gòu)造函數(shù)return之前,插入一個StoreStore屏障。這個屏障禁止處理器把final域的寫重排序到構(gòu)造函數(shù)之外。

現(xiàn)在讓我們分析writer方法,writer方法只包含一行代碼obj = new FinalTest();這行代碼包含兩個步驟:

1)構(gòu)造一個FinalTest類型的對象

2)把這個對象的引用賦值給obj

假設(shè)線程B的讀對象引用與讀對象的成員域之間沒有重排序,下圖是一種可能的執(zhí)行時序

Java內(nèi)存區(qū)域與內(nèi)存模型詳解

在上圖中,寫普通域的操作被編譯器重排序到了構(gòu)造函數(shù)之外,讀線程B錯誤的讀取到了普通變量i初始化之前的值。而寫final域的操作被寫final域重排序的規(guī)則限定在了構(gòu)造函數(shù)之內(nèi),讀線程B正確的讀取到了final變量初始化之后的值。

寫final域的重排序規(guī)則可以確保:在對象引用為任意線程可見之前,對象的final域已經(jīng)被初始化了,而普通變量不具有這個保證。以上圖為例,讀線程B看到對象obj的時候,很可能obj對象還沒有構(gòu)造完成(對普通域i的寫操作被重排序到構(gòu)造函數(shù)外,此時初始值1還沒有寫入普通域i)

讀final域的重排序規(guī)則是:在一個線程中,初次讀對象的引用與初次讀這個對象包含的final域,JMM禁止重排序這兩個操作(該規(guī)則僅僅針對處理器)。編譯器會在讀final域的操作前面加一個LoadLoad屏障。

初次讀對象引用與初次讀該對象包含的final域,這兩個操作之間存在間接依賴關(guān)系。由于編譯器遵守間接依賴關(guān)系,因此編譯器不會重排序這兩個操作。大多數(shù)處理器也會遵守間接依賴,也不會重排序這兩個操作。但有少數(shù)處理器允許對存在間接依賴關(guān)系的操作做重排序(比如alpha處理器),這個規(guī)則就是專門用來針對這種處理器的。

上面的例子中,reader方法包含三個操作

1)初次讀引用變量obj

2)初次讀引用變量指向?qū)ο蟮钠胀ㄓ?/p>

3)初次讀引用變量指向?qū)ο蟮膄inal域

現(xiàn)在假設(shè)寫線程A沒有發(fā)生任何重排序,同時程序在不遵守間接依賴的處理器上執(zhí)行,下圖是一種可能的執(zhí)行時序:

Java內(nèi)存區(qū)域與內(nèi)存模型詳解

在上圖中,讀對象的普通域操作被處理器重排序到讀對象引用之前。在讀普通域時,該域還沒有被寫線程寫入,這是一個錯誤的讀取操作,而讀final域的重排序規(guī)則會把讀對象final域的操作“限定”在讀對象引用之后,此時該final域已經(jīng)被A線程初始化過了,這是一個正確的讀取操作。

讀final域的重排序規(guī)則可以確保:在讀一個對象的final域之前,一定會先讀包含這個final域的對象的引用。在這個示例程序中,如果該引用不為null,那么引用對象的final域一定已經(jīng)被A線程初始化過了。

final域為引用類型,上面我們看到的final域是基礎(chǔ)的數(shù)據(jù)類型,如果final域是引用類型呢?

public class FinalReferenceTest { final int[] arrs;//final引用
    static FinalReferenceTest obj; public FinalReferenceTest(){
        arrs = new int[1];//1
        arrs[0] = 1;//2
 } public static void write0(){//A線程
        obj = new FinalReferenceTest();//3
 } public static void write1(){//線程B
        obj.arrs[0] = 2;//4
 } public static void reader(){//C線程
        if(obj!=null){//5
            int temp =obj.arrs[0];//6
 }
    }
}

JMM可以確保讀線程C至少能看到寫線程A在構(gòu)造函數(shù)中對final引用對象的成員域的寫入。即C至少能看到數(shù)組下標(biāo)0的值為1。而寫線程B對數(shù)組元素的寫入,讀線程C可能看得到,也可能看不到。JMM不保證線程B的寫入對讀線程C可見,因為寫線程B和讀線程C之間存在數(shù)據(jù)競爭,此時的執(zhí)行結(jié)果不可預(yù)知。

如果想要確保讀線程C看到寫線程B對數(shù)組元素的寫入,寫線程B和讀線程C之間需要使用同步原語(lock或volatile)來確保內(nèi)存可見性。

前面我們提到過,寫final域的重排序規(guī)則可以確保:在引用變量為任意線程可見之前,該引用變量指向的對象的final域已經(jīng)在構(gòu)造函數(shù)中被正確初始化過了。其實,要得到這個效果,還需要一個保證:在構(gòu)造函數(shù)內(nèi)部,不能讓這個被構(gòu)造對象的引用為其他線程所見,也就是對象引用不能在構(gòu)造函數(shù)中“逸出”。

public class FinalReferenceEscapeExample {final int i;static FinalReferenceEscapeExample obj;public FinalReferenceEscapeExample () {
    i = 1; // 1寫final域
    obj = this; // 2 this引用在此"逸出"
  }
  public static void writer() {new FinalReferenceEscapeExample ();
  }public static void reader() {if (obj != null) { // 3
      int temp = obj.i; // 4
    }
  }
}

假設(shè)一個線程A執(zhí)行writer()方法,另一個線程B執(zhí)行reader()方法。這里的操作2使得對象還未完成構(gòu)造前就為線程B可見。即使這里的操作2是構(gòu)造函數(shù)的最后一步,且在程序中操作2排在操作1后面,執(zhí)行read()方法的線程仍然可能無法看到final域被初始化后的值,因為這里的操作1和操作2之間可能被重排序。

JSR-133為什么要增強final的語義:

通過為final域增加寫和讀重排序規(guī)則,可以為Java程序員提供初始化安全保證:只要對象是正確構(gòu)造的(被構(gòu)造對象的引用在構(gòu)造函數(shù)中沒有“逸出”),那么不需要使用同步(指lock和volatile的使用)就可以保證任意線程都能看到這個final域在構(gòu)造函數(shù)中被初始化之后的值。

五、JMM是如何處理并發(fā)過程中的三大特性

JMM是圍繞這在并發(fā)過程中如何處理原子性、可見性和有序性這3個特性來建立的。

  • 原子性:

    Java中,對基本數(shù)據(jù)類型的讀取和賦值操作是原子性操作,所謂原子性操作就是指這些操作是不可中斷的,要做一定做完,要么就沒有執(zhí)行。比如:

    i = 2;j = i;i++;i = i + 1;

    上面4個操作中,i=2是讀取操作,必定是原子性操作,j=i你以為是原子性操作,其實吧,分為兩步,一是讀取i的值,然后再賦值給j,這就是2步操作了,稱不上原子操作,i++和i = i + 1其實是等效的,讀取i的值,加1,再寫回主存,那就是3步操作了。所以上面的舉例中,最后的值可能出現(xiàn)多種情況,就是因為滿足不了原子性。

    JMM只能保證對單個volatile變量的讀/寫具有原子性,但類似于volatile++這種符合操作不具有原子性,這時候就必須借助于synchronized和Lock來保證整塊代碼的原子性了。線程在釋放鎖之前,必然會把i的值刷回到主存的。

  • 可見性:可見性指當(dāng)一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。Java內(nèi)存模型是通過在變量修改后將新值同步回主內(nèi)存,在變量讀取前從主內(nèi)存刷新變量值這種依賴主內(nèi)存作為傳遞媒介的方式來實現(xiàn)可見性。

  • 無論是普通變量還是volatile變量,它們的區(qū)別是:volatile的特殊規(guī)則保證了新值能立即同步到主內(nèi)存,以及每次使用前立即從主內(nèi)存刷新。因為,可以說volatile保證了多線程操作時變量的可見性,而普通變量不能保證這一點。

  • 除了volatile之外,java中還有2個關(guān)鍵字能實現(xiàn)可見性,即synchronized和final(final修飾的變量,線程安全級別最高)。同步塊的可見性是由“對一個變量執(zhí)行unlock操作之前,必須先把此變量同步回主內(nèi)存中(執(zhí)行store,write操作)”這條規(guī)則獲得;而final關(guān)鍵字的可見性是指:被final修飾的字段在構(gòu)造器中一旦初始化完成,并且構(gòu)造器沒有把“this”的引用傳遞出去(this引用逃逸是一件很危險的事,其他線程有可能通過這個引用訪問到“初始化了一半”的對象),那么在其他線程中就能看到final字段的值。

  • 有序性:JMM的有序性在講解volatile時詳細的討論過,java程序中天然的有序性可以總結(jié)為一句話:如果在本線程內(nèi)觀察,所有操作都是有序的;如果在一個線程中觀察另一個線程,所有操作都是無序的。前半句是指“線程內(nèi)表現(xiàn)為串行的語義”,后半句指的是“指令重排”現(xiàn)象和“工作內(nèi)存與主內(nèi)存同步延遲”現(xiàn)象。

  • 前半句可以用JMM規(guī)定的as-if-serial語義來解決,后半句可以用JMM規(guī)定的happens-before原則來解決。Java語義提供了volatile和synchronized兩個關(guān)鍵字來保證線程之間操作的有序性,volatile關(guān)鍵字本身就包含了禁止指令重排的語義,而synchronized則是由“一個變量在同一個時刻只允許一條線程對其進行l(wèi)ock操作”這條規(guī)則獲取的。這個規(guī)則決定了持有同一個鎖的兩個同步塊只能串行的進入。

到此,關(guān)于“Java內(nèi)存區(qū)域與內(nèi)存模型詳解”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識,請繼續(xù)關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>

網(wǎng)站欄目:Java內(nèi)存區(qū)域與內(nèi)存模型詳解
網(wǎng)站鏈接:http://bm7419.com/article48/jdehep.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站設(shè)計軟件開發(fā)、做網(wǎng)站移動網(wǎng)站建設(shè)、虛擬主機、小程序開發(fā)

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)

成都網(wǎng)站建設(shè)公司