轉(zhuǎn)載于:https://mp.weixin.qq.com/s/EhIJpxRUb26KCJqpFbBCrA
1 JMM
1.1 問題引入
為什么要有內(nèi)存模型
要想回答這個問題,我們需要先弄懂傳統(tǒng)計算機(jī)硬件內(nèi)存架構(gòu)。好了,要開始畫圖了
硬件內(nèi)存架構(gòu)圖
含有一二三級架構(gòu)的內(nèi)存架構(gòu)圖
1.2 CPU模型
去過機(jī)房的同學(xué)都知道,一般在大型服務(wù)器上會配置多個CPU,每個CPU還會有多個核,這就意味著多個CPU或者多個核可以同時(并發(fā))工作。如果使用Java 起了一個多線程的任務(wù),很有可能每個 CPU 都會跑一個線程,那么你的任務(wù)在某一刻就是真正并發(fā)執(zhí)行了。
1.2.1 CPU Register
CPU Register也就是 CPU 寄存器。CPU寄存器是 CPU 內(nèi)部集成的,在寄存器上執(zhí)行操作的效率要比在主存上高出幾個數(shù)量級
在CPU中至少要有六類寄存器:指令寄存器(IR)、程序計數(shù)器(PC)、地址寄存器(AR)、數(shù)據(jù)寄存器(DR)、累加寄存器(AC)、程序狀態(tài)字寄存器(PSW)。這些寄存器用來暫存一個計算機(jī)字,其數(shù)目可以根據(jù)需要進(jìn)行擴(kuò)充
按與CPU遠(yuǎn)近來分,離得最近的是寄存器,然后緩存,最后內(nèi)存。所以,寄存器是最貼近CPU的,而且CPU只與寄存器中進(jìn)行存取。寄存器從內(nèi)存中讀取數(shù)據(jù),但由于寄存器和內(nèi)存讀取速度相差太大,所以有了緩存。即讀取數(shù)據(jù)的方式為:
CPU〈------〉寄存器〈---->緩存<----->內(nèi)存
當(dāng)寄存器沒有從緩存中讀取到數(shù)據(jù)時,也就是沒有命中,那么就從內(nèi)存中讀取數(shù)據(jù)
1.2.2 CPU Cache Memory
CPU Cache Memory也就是CPU 高速緩存。相對于硬盤讀取速度來說內(nèi)存讀取的效率非常高,但是與 CPU 還是相差數(shù)量級,所以在 CPU 和主存間引入了多級緩存,目的是為了做一下緩沖。
CPU內(nèi)部集成的緩存稱為一級緩存(L1 Cache),外部的稱為二級緩存(L2 Cache)。
一級緩存中又分為數(shù)據(jù)緩存(D-Cache)和指令緩存(I-Cache)。二者可以同時被CPU進(jìn)行訪問,減少了爭用Cache所造成的沖突,提高了CPU的效能。
CPU的一級緩存通常都是靜態(tài)RAM(Static RAM/SRAM),速度非常快,但是貴
二級緩存是CPU性能表現(xiàn)的關(guān)鍵之一,在CPU核心不變化的情況下,增加二級緩存容量能使性能大幅度提高。而同一核心的CPU高低端之分往往也是在二級緩存上存在差異
三級緩存是為讀取二級緩存后未命中的數(shù)據(jù)設(shè)計的一種緩存,在擁有三級緩存的CPU中,只有約5%的數(shù)據(jù)需要從內(nèi)存中調(diào)用,這進(jìn)一步提高了CPU的效率,從某種意義上說,預(yù)取效率的提高,大大降低了生產(chǎn)成本卻提供了非常接近理想狀態(tài)的性能
1.2.3 Main Memory
Main Memory 就是主存,主存比 L1、L2 緩存要大很多
注意:部分高端機(jī)器還有 L3 三級緩存。
內(nèi)存中相關(guān)概念:
- ROM(Read Only Memory)
只讀儲存器 ,對于電腦來講就是硬盤,在系統(tǒng)停止供電的時候仍然可以保持?jǐn)?shù)據(jù) - PROM
PROM是可編程的ROM,PROM和EPROM(可擦除可編程ROM)兩者區(qū)別是,PROM是一次性的,也就是軟件灌入后,就無法修改了,現(xiàn)在已經(jīng)不可能使用了,而EPROM是通過紫外光的照射擦除原先的程序,是一種通用的存儲器。另外一種EEPROM是通過電子擦除,價格很高,寫入時間很長,寫入很慢。 - RAM(Random Access Memory)
隨機(jī)儲存器 ,就是電腦內(nèi)存條。用于存放動態(tài)數(shù)據(jù)。(也叫運行內(nèi)存)系統(tǒng)運行的時候,需要把操作系統(tǒng)從ROM中讀取出來,放在RAM中運行,而RAM通常都是在掉電之后就丟失數(shù)據(jù),典型的RAM就是計算機(jī)的內(nèi)存 - 靜態(tài)RAM(Static RAM/SRAM)
當(dāng)數(shù)據(jù)被存入其中后不會消失。SRAM速度非???,是目前讀寫最快的存儲設(shè)備。當(dāng)這個SRAM單元被賦予0 或者1 的狀態(tài)之后,它會保持這個狀態(tài)直到下次被賦予新的狀態(tài)或者斷電之后才會更改或者消失。需要4-6 只晶體管實現(xiàn), 價格昂貴。
一級,二級,三級緩存都是使用SRAM - 動態(tài)RAM(Dynamic RAM/DRAM)
DRAM必須在一定的時間內(nèi)不停的刷新才能保持其中存儲的數(shù)據(jù)。DRAM只要1 只晶體管就可以實現(xiàn)。
DRAM保留數(shù)據(jù)的時間很短,速度也比SRAM慢,不過它還是比任何的ROM都要快,但從價格上來說DRAM相比SRAM要便宜很 多,
計算機(jī)內(nèi)存就是DRAM的
1.2.4 主存存取原理
目前計算機(jī)使用的主存基本都是隨機(jī)讀寫存儲器(RAM),現(xiàn)代RAM的結(jié)構(gòu)和存取原理比較復(fù)雜,這里本文拋卻具體差別,抽象出一個十分簡單的存取模型來說明RAM的工作原理。

從抽象角度看,主存是一系列的存儲單元組成的矩陣,每個存儲單元存儲固定大小的數(shù)據(jù)。每個存儲單元有唯一的地址,現(xiàn)代主存的編址規(guī)則比較復(fù)雜,這里將其簡化成一個二維地址:通過一個行地址和一個列地址可以唯一定位到一個存儲單元。上圖展示了一個4 x 4的主存模型。
主存的存取過程如下:
- 當(dāng)系統(tǒng)需要讀取主存時,則將地址信號放到地址總線上傳給主存,主存讀到地址信號后,解析信號并定位到指定存儲單元,然后將此存儲單元數(shù)據(jù)放到數(shù)據(jù)總線上,供其它部件讀取。
- 寫主存的過程類似,系統(tǒng)將要寫入單元地址和數(shù)據(jù)分別放在地址總線和數(shù)據(jù)總線上,主存讀取兩個總線的內(nèi)容,做相應(yīng)的寫操作。
這里可以看出,主存存取的時間僅與存取次數(shù)呈線性關(guān)系,因為不存在機(jī)械操作,兩次存取的數(shù)據(jù)的“距離”不會對時間有任何影響,例如,先取A0再取A1和先取A0再取D3的時間消耗是一樣的。
1.2.5 磁盤存取原理
與主存不同,磁盤I/O存在機(jī)械運動耗費,因此磁盤I/O的時間消耗是巨大的。
下圖是磁盤的整體結(jié)構(gòu)示意圖。

一個磁盤由大小相同且同軸的圓形盤片組成,磁盤可以轉(zhuǎn)動(各個磁盤必須同步轉(zhuǎn)動)。在磁盤的一側(cè)有磁頭支架,磁頭支架固定了一組磁頭,每個磁頭負(fù)責(zé)存取一個磁盤的內(nèi)容。磁頭不能轉(zhuǎn)動,但是可以沿磁盤半徑方向運動(實際是斜切向運動),每個磁頭同一時刻也必須是同軸的,即從正上方向下看,所有磁頭任何時候都是重疊的(不過目前已經(jīng)有多磁頭獨立技術(shù),可不受此限制)。
下圖是磁盤結(jié)構(gòu)的示意圖。

盤片被劃分成一系列同心環(huán),圓心是盤片中心,每個同心環(huán)叫做一個磁道,所有半徑相同的磁道組成一個柱面。磁道被沿半徑線劃分成一個個小的段,每個段叫做一個扇區(qū),每個扇區(qū)是磁盤的最小存儲單元。為了簡單起見,我們下面假設(shè)磁盤只有一個盤片和一個磁頭。
當(dāng)需要從磁盤讀取數(shù)據(jù)時,系統(tǒng)會將數(shù)據(jù)邏輯地址傳給磁盤,磁盤的控制電路按照尋址邏輯將邏輯地址翻譯成物理地址,即確定要讀的數(shù)據(jù)在哪個磁道,哪個扇區(qū)。為了讀取這個扇區(qū)的數(shù)據(jù),需要將磁頭放到這個扇區(qū)上方,為了實現(xiàn)這一點,磁頭需要移動對準(zhǔn)相應(yīng)磁道,這個過程叫做尋道,所耗費時間叫做尋道時間,然后磁盤旋轉(zhuǎn)將目標(biāo)扇區(qū)旋轉(zhuǎn)到磁頭下,這個過程耗費的時間叫做旋轉(zhuǎn)時間。
1.2.6 局部性原理與磁盤預(yù)讀
由于存儲介質(zhì)的特性,磁盤本身存取就比主存慢很多,再加上機(jī)械運動耗費,磁盤的存取速度往往是主存的幾百分分之一,因此為了提高效率,要盡量減少磁盤I/O。為了達(dá)到這個目的,磁盤往往不是嚴(yán)格按需讀取,而是每次都會預(yù)讀,即使只需要一個字節(jié),磁盤也會從這個位置開始,順序向后讀取一定長度的數(shù)據(jù)放入內(nèi)存。
這樣做的理論依據(jù)是計算機(jī)科學(xué)中著名的局部性原理:
當(dāng)一個數(shù)據(jù)被用到時,其附近的數(shù)據(jù)也通常會馬上被使用。程序運行期間所需要的數(shù)據(jù)通常比較集中。由于磁盤順序讀取的效率很高(不需要尋道時間,只需很少的旋轉(zhuǎn)時間),因此對于具有局部性的程序來說,預(yù)讀可以提高I/O效率。
預(yù)讀的長度一般為頁(page)的整倍數(shù)。頁是計算機(jī)管理存儲器的邏輯塊,硬件及操作系統(tǒng)往往將主存和磁盤存儲區(qū)分割為連續(xù)的大小相等的塊,每個存儲塊稱為一頁(在許多操作系統(tǒng)中,頁得大小通常為4k),主存和磁盤以頁為單位交換數(shù)據(jù)。當(dāng)程序要讀取的數(shù)據(jù)不在主存中時,會觸發(fā)一個缺頁異常,此時系統(tǒng)會向磁盤發(fā)出讀盤信號,磁盤會找到數(shù)據(jù)的起始位置并向后連續(xù)讀取一頁或幾頁載入內(nèi)存中,然后異常返回,程序繼續(xù)運行。
1.3 緩存一致性問題
由于主存與 CPU 處理器的運算能力之間有數(shù)量級的差距,所以在傳統(tǒng)計算機(jī)內(nèi)存架構(gòu)中會引入高速緩存來作為主存和處理器之間的緩沖,CPU 將常用的數(shù)據(jù)放在高速緩存中,運算結(jié)束后 CPU 再將運算結(jié)果同步到主存中
使用高速緩存解決了 CPU 和主存速率不匹配的問題,但同時又引入另外一個新問題:緩存一致性問題
在多
CPU的系統(tǒng)中(或者單CPU多核的系統(tǒng)),每個CPU內(nèi)核都有自己的高速緩存,它們共享同一主內(nèi)存(Main Memory)。當(dāng)多個CPU的運算任務(wù)都涉及同一塊主內(nèi)存區(qū)域時,CPU 會將數(shù)據(jù)讀取到緩存中進(jìn)行運算,這可能會導(dǎo)致各自的緩存數(shù)據(jù)不一致。
因此需要每個CPU 訪問緩存時遵循一定的協(xié)議,在讀寫數(shù)據(jù)時根據(jù)協(xié)議進(jìn)行操作,共同來維護(hù)緩存的一致性。這類協(xié)議有 MSI、MESI、MOSI、和 Dragon Protocol 等
緩存一致性和原子操作
1.4 處理器優(yōu)化和指令重排序
為了提升性能在 CPU 和主內(nèi)存之間增加了高速緩存,但在多線程并發(fā)場景可能會遇到緩存一致性問題。那還有沒有辦法進(jìn)一步提升 CPU 的執(zhí)行效率呢?答案是:處理器優(yōu)化
為了使處理器內(nèi)部的運算單元能夠最大化被充分利用,處理器會對輸入代碼進(jìn)行亂序執(zhí)行處理,這就是處理器優(yōu)化
除了處理器會對代碼進(jìn)行優(yōu)化處理,很多現(xiàn)代編程語言的編譯器也會做類似的優(yōu)化

處理器優(yōu)化其實也是重排序的一種類型,這里總結(jié)一下,重排序可以分為三種類型:
- 編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語義放入前提下,可以重新安排語句的執(zhí)行順序。
- 指令級并行的重排序?,F(xiàn)代處理器采用了指令級并行技術(shù)來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性,處理器可以改變語句對應(yīng)機(jī)器指令的執(zhí)行順序
- 內(nèi)存系統(tǒng)的重排序。由于處理器使用緩存和讀寫緩沖區(qū),這使得
加載和存儲操作看上去可能是在亂序執(zhí)行
1.5 并發(fā)編程的問題
上面講了一堆硬件相關(guān)的東西,有些同學(xué)可能會有點懵,繞了這么大圈,這些東西跟 Java 內(nèi)存模型有啥關(guān)系嗎?不要急咱們慢慢往下看
熟悉 Java 并發(fā)的同學(xué)肯定對這三個問題很熟悉:可見性問題、原子性問題、有序性問題。如果從更深層次看這三個問題,其實就是上面講的緩存一致性、處理器優(yōu)化、指令重排序造成的
緩存一致性問題其實就是可見性問題,處理器優(yōu)化可能會造成原子性問題,指令重排序會造成有序性問題,你看是不是都聯(lián)系上了
出了問題總是要解決的,那有什么辦法呢?首先想到簡單粗暴的辦法,干掉緩存讓 CPU 直接與主內(nèi)存交互就解決了可見性問題,禁止處理器優(yōu)化和指令重排序就解決了原子性和有序性問題,但這樣一夜回到解放前了,顯然不可取。
所以技術(shù)前輩們想到了在物理機(jī)器上定義出一套內(nèi)存模型, 規(guī)范內(nèi)存的讀寫操作。內(nèi)存模型解決并發(fā)問題主要采用兩種方式:限制處理器優(yōu)化和使用內(nèi)存屏障
1.5.1 可見性
可見性:當(dāng)一個線程修改了共享變量的值,其他線程會馬上知道這個修改。當(dāng)其他線程要讀取這個變量的時候,最終會去內(nèi)存中讀取,而不是從緩存中讀取
當(dāng)對非
volatile變量進(jìn)行讀寫時,每個線程從內(nèi)存拷貝變量到CPU緩存中。如果計算機(jī)有多個CPU,每個線程可能在不同的CPU上被處理,這意味著每個線程可以拷貝到不同的CPU緩存中。而聲明變量是volatile的,JVM保證了每次讀變量都從內(nèi)存中讀,跳過了CPU cache這一步
1.5.2 原子性
原子性:即一個操作或者多個操作,要么全部執(zhí)行并且不被打斷,要么就都不執(zhí)行
對變量的寫操作不依賴于當(dāng)前值才是原子級別的,在多線程環(huán)境中才可以不用考慮多并發(fā)問題。比如:n=n+1、n++ 就不行。n=m+1才是原子級別的,實在沒把握就使用synchronized關(guān)鍵字來代替volatile關(guān)鍵字
1.5.3 有序性
有序性:虛擬機(jī)在進(jìn)行代碼編譯時,對于那些改變順序之后不會對最終結(jié)果造成影響的代碼,虛擬機(jī)不一定會按照我們寫的代碼的順序來執(zhí)行,有可能將他們重排序。實際上,對于有些代碼進(jìn)行重排序之后,雖然對變量的值沒有造成影響,但有可能會出現(xiàn)線程安全問題。
volatile本身就包含了禁止指令重排序的語義,而synchronized關(guān)鍵字是由一個變量在同一時刻只允許一條線程對其進(jìn)行l(wèi)ock操作這條規(guī)則明確的
synchronized的特點,一個線程執(zhí)行互斥代碼過程如下:
- 獲得同步鎖;
- 清空工作內(nèi)存;
- 從主內(nèi)存拷貝對象副本到工作內(nèi)存;
- 執(zhí)行代碼(計算或者輸出等);
- 刷新主內(nèi)存數(shù)據(jù);
- 釋放同步鎖
1.6 介紹JMM
1.6.1 JMM定義
Java內(nèi)存模型可以理解為在特定的操作協(xié)議下,對特定的內(nèi)存或者高速緩存進(jìn)行讀寫訪問的過程抽象描述,不同架構(gòu)下的物理機(jī)擁有不一樣的內(nèi)存模型,Java虛擬機(jī)是一個實現(xiàn)了跨平臺的虛擬系統(tǒng),因此它也有自己的內(nèi)存模型,即Java內(nèi)存模型(Java Memory Model, JMM)
Java 內(nèi)存模型是一種規(guī)范,定義了很多東西:
- 所有的變量都存儲在主內(nèi)存(
Main Memory)中 - 每個線程都有一個私有的本地內(nèi)存(
Local Memory),本地內(nèi)存中存儲了該線程以讀/寫共享變量的拷貝副本 - 線程對變量的所有操作都必須在本地內(nèi)存中進(jìn)行,而不能直接讀寫主內(nèi)存。
-
不同的線程之間無法直接訪問對方本地內(nèi)存中的變量
在這里插入圖片描述
1.6.2 線程間通信
如果兩個線程都對一個共享變量進(jìn)行操作,共享變量初始值為 1,每個線程都變量進(jìn)行加 1,預(yù)期共享變量的值為 3。在 JMM 規(guī)范下會有一系列的操作
為了更好的控制主內(nèi)存和本地內(nèi)存的交互,
Java 內(nèi)存模型定義了八種操作來實現(xiàn):
-
lock:鎖定,作用于主內(nèi)存的變量,把一個變量標(biāo)識為一條線程獨占狀態(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í)行這個操作 -
store:存儲,作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個變量的值傳送到主內(nèi)存中,以便隨后的write的操作 -
write:寫入,作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中一個變量的值傳送到主內(nèi)存的變量中
注意:工作內(nèi)存也就是本地內(nèi)存的意思
對被
volatile修飾的變量進(jìn)行操作時,需要滿足以下規(guī)則:
- 規(guī)則1:線程對變量執(zhí)行的前一個動作是
load時才能執(zhí)行use,反之只有后一個動作是use時才能執(zhí)行load。線程對變量的read,load,use動作關(guān)聯(lián),必須連續(xù)一起出現(xiàn)。-----這保證了線程每次使用變量時都需要從主存拿到最新的值,保證了其他線程修改的變量本線程能看到。 - 規(guī)則2:線程對變量執(zhí)行的前一個動作是
assign時才能執(zhí)行store,反之只有后一個動作是store時才能執(zhí)行assign。線程對變量的assign,store,write動作關(guān)聯(lián),必須連續(xù)一起出現(xiàn)。-----這保證了線程每次修改變量后都會立即同步回主內(nèi)存,保證了本線程修改的變量其他線程能看到。 - 規(guī)則3:有線程T,變量V、變量W。假設(shè)動作A是T對V的
use或assign動作,P是根據(jù)規(guī)則2、3與A關(guān)聯(lián)的read或write動作;動作B是T對W的use或assign動作,Q是根據(jù)規(guī)則2、3與B關(guān)聯(lián)的read或write動作。如果A先與B,那么P先與Q。------這保證了volatile修飾的變量不會被指令重排序優(yōu)化,代碼的執(zhí)行順序與程序的順序相同
1.6.3 Java運行時內(nèi)存區(qū)域與硬件內(nèi)存的關(guān)系
了解過 JVM 的同學(xué)都知道,JVM 運行時內(nèi)存區(qū)域是分片的,分為棧、堆等,其實這些都是 JVM 定義的邏輯概念。在傳統(tǒng)的硬件內(nèi)存架構(gòu)中是沒有棧和堆這種概念
從圖中可以看出棧和堆既存在于高速緩存中又存在于主內(nèi)存中,所以兩者并沒有很直接的關(guān)系
2 JMM總結(jié)
由于CPU 和主內(nèi)存間存在數(shù)量級的速率差,想到了引入了多級高速緩存的傳統(tǒng)硬件內(nèi)存架構(gòu)來解決,多級高速緩存作為 CPU 和主內(nèi)間的緩沖提升了整體性能。解決了速率差的問題,卻又帶來了緩存一致性問題。
數(shù)據(jù)同時存在于高速緩存和主內(nèi)存中,如果不加以規(guī)范勢必造成災(zāi)難,因此在傳統(tǒng)機(jī)器上又抽象出了內(nèi)存模型
Java 語言在遵循內(nèi)存模型的基礎(chǔ)上推出了 JMM 規(guī)范,目的是解決由于多線程通過共享內(nèi)存進(jìn)行通信時,存在的本地內(nèi)存數(shù)據(jù)不一致、編譯器會對代碼指令重排序、處理器會對代碼亂序執(zhí)行等帶來的問題