1.前言
前面我們了解JVM的一些基礎(chǔ)知識,了解到了JVM是如何存儲,加載和處理數(shù)據(jù)的;今天來了解下JAVA內(nèi)存模型,學(xué)習(xí)是如何Java程序訪問內(nèi)存的
就java內(nèi)存模型而言,它時深入了解java并發(fā)編程的先決條件,對于后續(xù)多線程中的多線程安全,同步,異步處理更是大有裨益
2.目錄

3.硬件內(nèi)存架構(gòu)
在學(xué)習(xí)java內(nèi)存模型之前,先了解一下計(jì)算機(jī)硬件內(nèi)存模型.我們都知道處理器與計(jì)算機(jī)存儲設(shè)備運(yùn)算速度有幾個數(shù)量級的差別,總不能讓處理器總是等待計(jì)算機(jī)存儲設(shè)備,這樣就沒辦法顯現(xiàn)處處理器的優(yōu)勢
為了"壓榨"處理器的性能,達(dá)到"高并發(fā)"的效果,在處理器和存儲設(shè)備之間加入了高速緩存(cache)來作為緩沖

將運(yùn)算需要使用到的數(shù)據(jù)復(fù)制到緩存中,讓運(yùn)算能夠快速進(jìn)行.當(dāng)運(yùn)算完成之后,再將緩存中的結(jié)果寫入主內(nèi)存,這樣運(yùn)算器就不用等待主內(nèi)存的讀寫操作了
每個處理器都有自己的高速緩存,同時又共同操作同一塊主內(nèi)存,當(dāng)多個處理器同時操作主內(nèi)存時,可能導(dǎo)致數(shù)據(jù)不一致,因此需要"緩存一致性協(xié)議來"來保障.比如,MSI,MESI等
4.JAVA內(nèi)存模型
java內(nèi)存模型即Java Memory Model,簡稱JMM,用來屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,以實(shí)現(xiàn)讓Java程序在各平臺下都能夠達(dá)到一致的內(nèi)存訪問效果
JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲在主內(nèi)存(main memory)中,每個線程都有一個私有的本地內(nèi)存(local memory),本地內(nèi)存中存儲了該線程以讀/寫共享變量的副本.本地內(nèi)存是JMM的一個抽象概念,并不真實(shí)存在.它涵蓋了緩存,寫緩沖層,寄存器以及其他的硬件和編譯器優(yōu)化

JMM與Java內(nèi)存結(jié)構(gòu)并不是同一個層次的內(nèi)存劃分,兩者基本上沒有關(guān)系.如果一定要勉強(qiáng)對應(yīng),那從變量,主內(nèi)存,工作內(nèi)存的定義看,主內(nèi)存主要對應(yīng)Java對重的對象實(shí)例數(shù)據(jù)部分,工作內(nèi)存則對應(yīng)虛擬機(jī)的部分區(qū)域

主內(nèi)存:主要存儲的是Java實(shí)例對象,所有線程創(chuàng)建的實(shí)例對象都存放在主內(nèi)存中,不管該實(shí)例對象是成員方法變量還是方法中的本地變量(也稱局部變量),當(dāng)然也包括了共享的類信息,常量,靜態(tài)變量,共享數(shù)據(jù)區(qū)域,多條線程堆同一個變量進(jìn)行訪問可能會發(fā)現(xiàn)線程安全問題
工作內(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)存,因此存儲在工作內(nèi)存的數(shù)據(jù)不存在線程安全問題

4.1.內(nèi)存之間的交互操作
線程的工作內(nèi)存中保存了被該線程使用到的變量的主內(nèi)存副本拷貝,線程對變量的所有操作都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫主內(nèi)存中的變量.不同的線程之間也無法直接訪問對方工作內(nèi)存中的變量,線程間的變量值的傳遞均需要通過主內(nèi)存來完成

上圖中,本地內(nèi)存A和B有主內(nèi)存中共享變量x的副本,初始值都為0.線程A執(zhí)行之后把x更新為1,存放在本地內(nèi)存A中.當(dāng)線程A和線程B需要通信時,線程A首先會把本地內(nèi)存中x=1值刷新到主內(nèi)存中,主內(nèi)存中的x值變?yōu)?.隨后,線程B到主內(nèi)存中去讀取更新后x值,線程B的本地內(nèi)存的x值也變?yōu)榱?
在此交互過程中,Java內(nèi)存模型定義了8種操作來完成,虛擬機(jī)實(shí)現(xiàn)必須保證每一種操作都是原子的,不可再拆分的(double和long類型除外)
- lock(鎖定):作用于主內(nèi)存的變量,它把一個變量的標(biāo)識為一條線程獨(dú)占的狀態(tài)
- unlock(解鎖):用用于主內(nèi)存的變量,它把一個處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定
- read(讀取):作用于主內(nèi)存的變量,它把一個變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便隨后的load動作使用
- load(載入):作用于工作內(nèi)存的變量,它把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中
- use(使用):作用于工作內(nèi)存的變量,它把工作內(nèi)存中的一個變量的值傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個需要使用到變量的值的字節(jié)碼指令時將會執(zhí)行這個操作
- assign(賦值):作用于工作內(nèi)存的變量,它把一個從執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個給變量賦值的字節(jié)碼指令時執(zhí)行這個操作
- write(寫入):作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中
如果需要把一個變量從主內(nèi)存復(fù)制到工作內(nèi)存,那就要順序地執(zhí)行read和load操作,如果要把變量從工作內(nèi)存同步回主內(nèi)存,就要順序地執(zhí)行store和write操作.注意,Java內(nèi)存模型只要求上述兩個操作必須按順序執(zhí)行,而沒有保證是連續(xù)執(zhí)行.也就是說read與load之間,store與write之間是可插入其他指令的,如對主內(nèi)存中的變量a,b進(jìn)行訪問時,一種可能出現(xiàn)順序是read a,read b,load b,load a.除此之外,Java內(nèi)存模型還規(guī)定了在執(zhí)行上述8中基本操作時必須滿足如下規(guī)則
- 不允許read和load,store和write操作之一單獨(dú)出現(xiàn),即不允許一個變量從主內(nèi)存讀取了但工作內(nèi)存不接受,或者從工作內(nèi)存發(fā)起回寫了但主內(nèi)存不接受的情況出現(xiàn)
- 不允許一個線程丟棄它的最近的assign操作,即變量在工作內(nèi)存中改變了之后必須把該變化同步回主內(nèi)存
- 不允許一個線程無原因地(沒有發(fā)生過任何assign操作)把數(shù)據(jù)從線程的工作內(nèi)存同步回主內(nèi)存
- 一個新的變量只能在主內(nèi)存中"誕生",不允許在工作內(nèi)存中直接使用一個未被初始化(load或assign)的變量,換句話說,就是對一個變量實(shí)施use,store操作之前,必須先執(zhí)行過了assign和load操作
- 一個變量在同一時刻只允許一條線程對其進(jìn)行l(wèi)ock操作,但lock操作可以被同一條線程重復(fù)執(zhí)行多次,多次執(zhí)行l(wèi)ock后,只有執(zhí)行相同次數(shù)的unlock操作,變量才會被解鎖
- 如果對一個變量執(zhí)行l(wèi)ock操作,那將會清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個變量前,需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值
- 如果一個變量事先沒有被lock操作鎖定,那就不允許對它執(zhí)行unlock操作,也不允許去unlock一個被其他線程鎖定住的變量
- 對一個變量執(zhí)行unlock操作之前,必須先把此變量同步回主內(nèi)存中(執(zhí)行store,write操作)
4.2.long和double型變量的特殊規(guī)則
Java內(nèi)存模型要求lock,unlock,read,load,assign,use,store,write這8個操作都具有原子性,但對于64位的數(shù)據(jù)類型(long或double),在模型中定義了一條相對寬松的規(guī)定,允許虛擬機(jī)將沒有被volatile修飾的64位數(shù)據(jù)的讀寫操作劃分為兩次32位的操作來進(jìn)行,即允許虛擬機(jī)實(shí)現(xiàn)選擇可以不保證64位數(shù)據(jù)類型的load,store,read,write這4個操作的原子性,即long和double的非原子性協(xié)定
如果多線程的情況下double或long類型并未聲明為volatile,可能會出現(xiàn)"半個變量"的數(shù)值,也就是既非原值,也非修改后的值
雖然Java規(guī)范允許上面的實(shí)現(xiàn),但商用虛擬機(jī)中基本都采用了原子性的操作,因此在日常使用中幾乎不會出現(xiàn)讀取到"半個變量"的情況
5.總結(jié)
本章講解了JAVA內(nèi)存模型,對于內(nèi)存的交互有了簡單的了解,下一章將講解Java內(nèi)存模型的特征及原則
原文:https://www.choupangxia.com/2019/11/04/interview-jvm-gc-05/