通過上篇文章【多線程與并發(fā)】Java并發(fā)理論基礎(chǔ)我們了解到:Java內(nèi)存模型是一種虛擬機規(guī)范,用于屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問差異茄唐。并從兩個維度去理解了它享钞。那么本篇文章就近距離的來認識下Java內(nèi)存模型岖寞。
從堆棧說起
JVM內(nèi)部使用的Java內(nèi)存模型在線程棧和堆之間劃分內(nèi)存恋拍。 此圖從邏輯角度說明了Java內(nèi)存模型:- 線程棧內(nèi)包含正在執(zhí)行的每個方法的所有局部變量嘀粱,線程的訪問是私有的,每個線程只能訪問自己棧豆同。
- 當(dāng)局部變量是基本類型(boolean,byte含鳞,short影锈,char,int蝉绷,long鸭廷,float,double)時熔吗,它完全保留在線程棧上辆床。
- 當(dāng)局部變量是對象的引用時,引用(局部變量)存儲在線程棧中磁滚,對象本身存儲在堆上佛吓。
- 對象的成員變量與對象本身一起存儲在堆(Heap)上,不論成員變量是基本數(shù)據(jù)類型還是對象引用類型垂攘。
- 靜態(tài)成員變量跟隨著類定義一起也存放在堆上维雇。
- 存放在堆上的對象可以被所有持有對這個對象引用的線程訪問。
- 當(dāng)一個線程可以訪問一個對象時晒他,它也可以訪問該對象的成員變量吱型。
-
如果兩個線程同時調(diào)用同一個對象上的同一個方法,那它們都可以訪問該對象的成員變量陨仅,但是每一個線程都擁有這個成員變量的副本津滞。
java-memory-model-3.png此圖能夠直觀地反映上述關(guān)于堆棧的幾點說明。
硬件內(nèi)存結(jié)構(gòu)
現(xiàn)代硬件內(nèi)存架構(gòu)與內(nèi)部Java內(nèi)存模型略有不同灼伤。
先來看一張現(xiàn)代計算機硬件架構(gòu)圖:通過上圖我們來對硬件內(nèi)存結(jié)構(gòu)進行詳細說明:
-
CPU:
- 現(xiàn)代計算機通常是有2個或多個CPU触徐。
- 其中一些CPU還可能是多核。在這種現(xiàn)代計算機上狐赡,可以同時運行多個線程撞鹉。
- 每個CPU都能夠在任何給定時間運行一個線程。這就意味著,如果Java應(yīng)用程序是多線程的鸟雏,線程是可能同時運行享郊。
-
寄存器:
- 每個CPU基本上都包含一組在CPU內(nèi)存中的寄存器。
- CPU訪問這些寄存器要比訪問主存儲器快的多孝鹊。
-
高速緩存存儲器:
- 每個CPU還可以具有CPU高速緩存存儲器層炊琉。
- 由于計算機內(nèi)存與CPU之間的運算速度有著巨大差距,現(xiàn)代計算機為了解決這一問題又活,在內(nèi)存和CPU之間加入了一層讀寫速度盡可能接近CPU運算速度的高速緩存來最為緩沖:將運算需要使用到的數(shù)據(jù)復(fù)制到緩存中苔咪,讓運算能快速進行。當(dāng)運算結(jié)束后再從緩存同步回內(nèi)存之中柳骄,這樣處理器就無需等待緩慢地內(nèi)存讀寫了悼泌。
- CPU訪問速度上:CPU內(nèi)部寄存器 > 高速緩存存儲器 > 內(nèi)存
- CPU可能有一個或者多個緩存層(級別1和級別2)(要了解Java內(nèi)存模型如何與內(nèi)存交互,這一點并不重要夹界,重要的是要知道CPU可以有某種緩存存儲層)馆里。
-
內(nèi)存:
- 計算機還包含主存儲區(qū)(RAM),所有CPU都可以訪問主內(nèi)存可柿。
- 主存儲區(qū)通常比CPU的高速緩存存儲器大得多鸠踪,同時訪問速度也就較慢。
通常复斥,當(dāng)CPU需要訪問主存儲器時营密,它會將部分主存儲器讀入其CPU緩存。甚至可以將部分緩存讀入其內(nèi)部寄存器目锭,然后對其執(zhí)行操作评汰。當(dāng)CPU需要講結(jié)果寫回主存儲器時,他會將值從其內(nèi)部寄存器刷新到高速緩沖存儲器痢虹,并在某個時間點將值刷入主存儲器被去。
硬件內(nèi)存結(jié)構(gòu)&CPU普及導(dǎo)致的問題
緩存一致性問題
如上所述,Java內(nèi)存模型和硬件內(nèi)存架構(gòu)是不同的奖唯,硬件內(nèi)存架構(gòu)不區(qū)分線程棧和堆惨缆。在硬件上,線程棧和堆都位于主存儲器中丰捷。線程棧和堆的一部分有時可能存在于CPU高速緩存和內(nèi)部寄存器中坯墨。如下圖:
當(dāng)對象和變量可以存儲在計算機的各種不同的存儲區(qū)域中時,可能會出現(xiàn)某些問題(統(tǒng)稱為:緩存一致性問題)病往,兩個主要問題:
線程更新(寫入)到共享變量的可見性
讀寫共享變量時的競態(tài)條件
對象共享后的可見性:
如果兩個或多個線程共享一個對象捣染,而沒有正確使用volatile聲明或同步,則一個線程對共享對象的更新可能對其他線程不可見停巷。
舉例說明:共享對象最初存儲在主存儲器中耍攘。 然后累贤,在CPU上運行的線程將共享對象讀入其CPU緩存中。 它在那里對共享對象進行了更改少漆。 只要CPU緩存尚未刷新回主內(nèi)存示损,共享對象的更改版本對于在其他CPU上運行的線程是不可見的脆贵。 這樣负懦,每個線程最終都可能擁有自己的共享對象副本,每個副本都位于不同的CPU緩存中肯尺。
競態(tài)條件:
如果兩個或多個線程共享一個對象逾滥,并且多個線程更新該共享對象中的變量掀亩,則可能會出現(xiàn)競態(tài)。
舉例說明:如果線程A和線程B將共享變量count讀入到各自CPU緩存中(不同的CPU緩存),而后捉超,線程A將count加1惜纸,線程B也做了同樣的事椭更。count被增加了兩次虑瀑,每個CPU緩存中一次湿滓,如果這些增加操作被順序的執(zhí)行,則變量count應(yīng)該增加兩次并將原始值 +2寫會主存儲器舌狗。但是叽奥,兩次增加都是在沒有適當(dāng)?shù)耐较虏l(fā)執(zhí)行的。無論是線程A還是線程B將count修改后的版本寫回到主存中取痛侍,修改后的值僅會被原值大1朝氓。
CPU 為了解決內(nèi)存緩存不一致性問題可以通過制定緩存一致協(xié)議(比如 MESI 協(xié)議open in new window)或者其他手段來解決。 這個緩存一致性協(xié)議指的是在 CPU 高速緩存與主內(nèi)存交互的時候需要遵守的原則和規(guī)范主届。不同的 CPU 中赵哲,使用的緩存一致性協(xié)議通常也會有所不同。
操作系統(tǒng)通過 內(nèi)存模型(Memory Model) 定義一系列規(guī)范來解決這個問題较坛。無論是 Windows 系統(tǒng),還是 Linux 系統(tǒng)扒最,它們都有特定的內(nèi)存模型丑勤。
指令重排
除了緩存一致性問題,還存在另外一種硬件問題吧趣,也比較重要:為了使 CPU 內(nèi)部的運算單元能夠盡量被充分利用法竞,處理器可能會對輸入的字節(jié)碼指令進行重排序處理,也就是處理器優(yōu)化再菊。除了 CPU 之外,很多編程語言的編譯器也會有類似的優(yōu)化颜曾,比如 Java虛擬機的即時編譯器(JIT)也會做指令重排纠拔。在上一篇文章【多線程與并發(fā)】Java并發(fā)理論基礎(chǔ) #有序性:重排序引起 這節(jié)中對指令重排有詳細說明,可自行查閱泛豪,這里就不在贅述稠诲。
Java內(nèi)存模型(JMM)
什么是Java內(nèi)存模型
Java 是最早嘗試提供內(nèi)存模型的編程語言。由于早期內(nèi)存模型存在一些缺陷(比如非常容易削弱編譯器的優(yōu)化能力)诡曙,從 Java5 開始臀叙,Java 開始使用新的內(nèi)存模型 《JSR-133:Java Memory Model and Thread Specification》open in new window 。
對于 Java 來說价卤,你可以把 JMM 看作是 Java 定義的并發(fā)編程相關(guān)的一組規(guī)范劝萤,除了抽象了線程和主內(nèi)存之間的關(guān)系之外,其還規(guī)定了從 Java 源代碼到 CPU 可執(zhí)行指令的這個轉(zhuǎn)化過程要遵守哪些和并發(fā)相關(guān)的原則和規(guī)范慎璧,其主要目的是為了簡化多線程編程床嫌,增強程序可移植性的。
為什么需要Java內(nèi)存模型
一般來說胸私,編程語言也可以直接復(fù)用操作系統(tǒng)層面的內(nèi)存模型厌处。不過,不同的操作系統(tǒng)內(nèi)存模型不同岁疼。如果直接復(fù)用操作系統(tǒng)層面的內(nèi)存模型阔涉,就可能會導(dǎo)致同樣一套代碼換了一個操作系統(tǒng)就無法執(zhí)行了。Java 語言是跨平臺的捷绒,它需要自己提供一套內(nèi)存模型以屏蔽系統(tǒng)差異瑰排。
在并發(fā)編程下,像 CPU 多級緩存和指令重排這類設(shè)計可能會導(dǎo)致程序運行出現(xiàn)一些問題暖侨。比如上面所提到的凶伙,為此,JMM 抽象了 happens-before 原則(在上一篇文章【多線程與并發(fā)】Java并發(fā)理論基礎(chǔ) 有詳細說明它碎,可自行查閱函荣,本篇文章會稍作補充說明)來解決這個指令重排序問題显押。
Java內(nèi)存模型通俗來說就是定義了一些規(guī)范來解決上面這些問題,開發(fā)者可以利用這些規(guī)范更方便地開發(fā)多線程程序傻挂。對于 Java 開發(fā)者說乘碑,你不需要了解底層原理,直接使用并發(fā)相關(guān)的一些關(guān)鍵字和類(比如
volatile
金拒、synchronized
兽肤、各種Lock
)即可開發(fā)出并發(fā)安全的程序。
Java內(nèi)存模型的抽象
JMM抽象了線程和主內(nèi)存之間的關(guān)系:
線程之間的共享變量存儲在內(nèi)存(Main Memory)中绪抛。
每個線程都有一個私有的本地內(nèi)存(Local Memory)资铡,本地內(nèi)存是JMM的一個抽象概念,并不真實存在幢码,它涵蓋了緩存笤休、寫緩沖區(qū)、寄存器以及其他的硬件和編譯優(yōu)化症副。本地內(nèi)存中存儲了該線程以讀/寫共享變量的副本店雅。
從更低的層次來說,主內(nèi)存就是硬件的內(nèi)存贞铣,為了獲取更好的運行速度闹啦,虛擬機及硬件系統(tǒng)可能會讓工作內(nèi)存優(yōu)先存儲于寄存器和高速緩存中。
Java 內(nèi)存模型的抽象示意圖如下:
從上圖來看辕坝,線程 A 與線程 B 之間如要通信的話窍奋,必須要經(jīng)歷下面 2 個步驟:
- 首先,線程 A 把本地內(nèi)存 A 中更新過的共享變量刷新到主內(nèi)存中去酱畅。
- 然后费变,線程 B 到主內(nèi)存中去讀取線程 A 之前已更新過的共享變量。
下面通過示意圖來說明這兩個步驟:
關(guān)于主內(nèi)存與工作內(nèi)存之間的具體交互協(xié)議圣贸,即一個變量如何從主內(nèi)存拷貝到工作內(nèi)存挚歧、如何從工作內(nèi)存同步到主內(nèi)存之間的實現(xiàn)細節(jié),Java內(nèi)存模型定義了以下八種操作來完成:
lock(鎖定): 作用于主內(nèi)存中的變量吁峻,將他標(biāo)記為一個線程獨享變量滑负。
unlock(解鎖): 作用于主內(nèi)存中的變量,解除變量的鎖定狀態(tài)用含,被解除鎖定狀態(tài)的變量才能被其他線程鎖定矮慕。
read(讀取):作用于主內(nèi)存的變量啄骇,它把一個變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中痴鳄,以便隨后的 load 動作使用。
load(載入):把 read 操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量的副本中缸夹。
use(使用):把工作內(nèi)存中的一個變量的值傳給執(zhí)行引擎痪寻,每當(dāng)虛擬機遇到一個使用到變量的指令時都會使用該指令螺句。
assign(賦值):作用于工作內(nèi)存的變量,它把一個從執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量橡类,每當(dāng)虛擬機遇到一個給變量賦值的字節(jié)碼指令時執(zhí)行這個操作蛇尚。
store(存儲):作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個變量的值傳送到主內(nèi)存中顾画,以便隨后的 write 操作使用取劫。
write(寫入):作用于主內(nèi)存的變量,它把 store 操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中研侣。
Java內(nèi)存模型還規(guī)定了在執(zhí)行上述八種基本操作時谱邪,必須滿足如下規(guī)則:
如果要把一個變量從主內(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操作之一單獨出現(xiàn)
不允許一個線程丟棄它的最近assign的操作基显,即變量在工作內(nèi)存中改變了之后必須同步到主內(nèi)存中。
不允許一個線程無原因地(沒有發(fā)生過任何assign操作)把數(shù)據(jù)從工作內(nèi)存同步回主內(nèi)存中善炫。
一個新的變量只能在主內(nèi)存中誕生撩幽,不允許在工作內(nèi)存中直接使用一個未被初始化(load或assign)的變量。即就是對一個變量實施use和store操作之前箩艺,必須先執(zhí)行過了assign和load操作窜醉。
一個變量在同一時刻只允許一條線程對其進行l(wèi)ock操作,但lock操作可以被同一條線程重復(fù)執(zhí)行多次艺谆,多次執(zhí)行l(wèi)ock后榨惰,只有執(zhí)行相同次數(shù)的unlock操作,變量才會被解鎖静汤。lock和unlock必須成對出現(xiàn)
如果對一個變量執(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操作)缠黍。
happens-before原則
happens-before 設(shè)計思想
happens-before 這個概念最早誕生于 Leslie Lamport 于 1978 年發(fā)表的論文《Time,Clocks and the Ordering of Events in a Distributed System》open in new window药蜻。在這篇論文中瓷式,Leslie Lamport 提出了邏輯時鐘open in new window的概念替饿,這也成了第一個邏輯時鐘算法 。在分布式環(huán)境中蒿往,通過一系列規(guī)則來定義邏輯時鐘的變化盛垦,從而能通過邏輯時鐘來對分布式系統(tǒng)中的事件的先后順序進行判斷。邏輯時鐘并不度量時間本身瓤漏,僅區(qū)分事件發(fā)生的前后順序腾夯,其本質(zhì)就是定義了一種 happens-before 關(guān)系。
JSR 133 引入了 happens-before 這個概念來描述兩個操作之間的內(nèi)存可見性蔬充。
為什么需要 happens-before 原則蝶俱? happens-before 原則的誕生是為了程序員和編譯器、處理器之間的平衡饥漫。程序員追求的是易于理解和編程的強內(nèi)存模型榨呆,遵守既定規(guī)則編碼即可。編譯器和處理器追求的是較少約束的弱內(nèi)存模型庸队,讓它們盡己所能地去優(yōu)化性能积蜻,讓性能最大化。happens-before 原則的設(shè)計思想其實非常簡單:
為了對編譯器和處理器的約束盡可能少彻消,只要不改變程序的執(zhí)行結(jié)果(單線程程序和正確執(zhí)行的多線程程序)竿拆,編譯器和處理器怎么進行重排序優(yōu)化都行。
對于會改變程序執(zhí)行結(jié)果的重排序宾尚,JMM 要求編譯器和處理器必須禁止這種重排序丙笋。
JMM 設(shè)計思想的示意圖:
了解了 happens-before 原則的設(shè)計思想,我們再來看看 JSR-133 對 happens-before 原則的定義:
如果一個操作 happens-before 另一個操作煌贴,那么第一個操作的執(zhí)行結(jié)果將對第二個操作可見御板,并且第一個操作的執(zhí)行順序排在第二個操作之前。
兩個操作之間存在 happens-before 關(guān)系牛郑,并不意味著 Java 平臺的具體實現(xiàn)必須要按照 happens-before 關(guān)系指定的順序來執(zhí)行怠肋。如果重排序之后的執(zhí)行結(jié)果與按 happens-before 關(guān)系來執(zhí)行的結(jié)果一致,那么 JMM 也允許這樣的重排序淹朋,否則不允許重排序笙各。
舉例說明:計算圓面積。
double pi = 3.14; //A
double r = 1.0; //B
double area = pi * r * r; //C
上面計算圓的面積的示例代碼存在三個 happens- before 關(guān)系:
A happens- before B
B happens- before C
A happens- before C
雖然 A happens-before B瑞你,但對 A 和 B 進行重排序不會影響代碼的執(zhí)行結(jié)果酪惭,所以 JMM 是允許編譯器和處理器執(zhí)行這種重排序的。但 A 和 B 必須是在 C 執(zhí)行之前者甲,也就是說 A和B happens-before C 春感。
happens-before 原則表達的意義其實并不是一個操作發(fā)生在另外一個操作的前面,雖然這從程序員的角度上來說也并無大礙嫩实。更準確地來說,它更想表達的意義是前一個操作的結(jié)果對于后一個操作是可見的甲献,無論這兩個操作是否在同一個線程中。
happens-before 規(guī)則
關(guān)于happens-before規(guī)則可在【多線程與并發(fā)】Java并發(fā)理論基礎(chǔ) 中自行查閱颂翼。
happens-before 與 JMM 的關(guān)系
happens-before 與 JMM 的關(guān)系如下圖所示:
如上圖所示晃洒,一個 happens-before 規(guī)則通常對應(yīng)于多個編譯器重排序規(guī)則和處理器重排序規(guī)則朦乏。對于 java 程序員來說,happens-before 規(guī)則簡單易懂呻疹,它避免程序員為了理解 JMM 提供的內(nèi)存可見性保證而去學(xué)習(xí)復(fù)雜的重排序規(guī)則以及這些規(guī)則的具體實現(xiàn)吃引。
再看并發(fā)編程三要素
可自行查閱上篇文章【多線程與并發(fā)】Java并發(fā)理論基礎(chǔ) #第二個理解維度:可見性,有序性刽锤,原子性 這一節(jié)镊尺,會不會有種豁然開朗的感覺呢并思?
參考: