前言
在開始進(jìn)入正題學(xué)習(xí)之前, 覺得有必要先來了解一下什么是計算機內(nèi)存模型, 然后再回頭看 java 內(nèi)存模型.
1. 計算機內(nèi)存模型
為什么要有內(nèi)存模型呢? 我們知道在計算機執(zhí)行程序的時候, 每條執(zhí)行都是在 CPU 中執(zhí)行的, 而執(zhí)行的時候, 又無法避免的和數(shù)據(jù)打交道. 而計算機上的數(shù)據(jù)是放在主內(nèi)存中的, 也可以理解為計算機的物理內(nèi)存. 隨著現(xiàn)代 CPU 技術(shù)的發(fā)展, CPU 的執(zhí)行速度越來越快, 而由于內(nèi)存的技術(shù)并沒有太大的變化, 所以從內(nèi)存中讀取和寫入數(shù)據(jù)的過程與 CPU 的執(zhí)行速度比起來差距就會越來越大, 這就導(dǎo)致 CPU 每次操作內(nèi)存都要消耗很多等待時間. 所以現(xiàn)代計算機系統(tǒng)不得不加入一層讀寫速度盡可能接近 CPU 運算速度的高速緩存(Cache)來作為內(nèi)存與 CPU 之間的緩沖.
那么程序的執(zhí)行過程就變成了: 程序在運行過程中, 將運算需要的數(shù)據(jù)從內(nèi)存中復(fù)制一份到 CPU 的高速緩存當(dāng)中, 那么 CPU 進(jìn)行計算時就可以直接從它的高速緩存讀取數(shù)據(jù)和向其中寫入數(shù)據(jù), 當(dāng)運算結(jié)束之后, 再將高速緩存中的數(shù)據(jù)刷新到主內(nèi)存中.
而隨著 CPU 能力不斷提升, 一層緩存已經(jīng)慢慢的變的無法滿足要求了, 于是就逐漸的衍生出多級緩存.
按照數(shù)據(jù)讀取順序和 CPU 結(jié)合的緊密程度, CPU 緩存可以分為一級緩存 L1, 二級緩存 L2, 三級緩存 L3, L0 為寄存器, 接下來是內(nèi)存, 本地磁盤, 遠(yuǎn)程存儲. 越向上緩存的存儲空間越小, 速度越快, 成本也就更高. 從上至下, 每一層都可以看做是更下一層的緩存, 即 L0 寄存器是 L1 一級緩存的緩存. 依次類推, 每一層的數(shù)據(jù)都是來自它的下一層. 所以每一層的數(shù)據(jù)是下一層數(shù)據(jù)的子集.
在現(xiàn)代 CPU 上, 一般來說 L0, L1, L2, L3 都繼承在 CPU 內(nèi)部, 同時 L1 還分為一級數(shù)據(jù)緩存和一級指令緩存, 分別用于存放數(shù)據(jù)和執(zhí)行數(shù)據(jù)的指令解碼. 每個核心擁有獨立的運算處理單元, 控制器, 寄存器, L1, L2 緩存, 然后一個 CPU 的多個核心共享最后一層 CPU 緩存 L3.
?
那么現(xiàn)在就會出現(xiàn)第一個問題: 緩存一致性問題.
在 CPU 和內(nèi)存之間增加緩存, 在多線程場景下會出現(xiàn)緩存一致性問題, 也就是說, 多個線程訪問進(jìn)程中的某個共享內(nèi)存, 且這多個線程分別在不同的核心上執(zhí)行, 那么每個核心都會在各自的高速緩存中保留一份共享內(nèi)存的緩存. 由于多核是可以并行的, 可能會出現(xiàn)多個線程同時寫各自緩存的情況, 那么就會造成同一個數(shù)據(jù)的緩存內(nèi)容可能不一致.
?
除了這種情況, 還有一種硬件問題, 這是出現(xiàn)的第二個問題: 指令重排問題.
為了使得 CPU 內(nèi)部的運算單元能盡量被充分利用, CPU 可能會對輸入代碼進(jìn)行亂序執(zhí)行優(yōu)化, 處理器會在計算之后將亂序執(zhí)行的結(jié)果重組. 保證該結(jié)果與順序執(zhí)行的結(jié)果是一致的, 但并不保證程序中各個語句計算的先后順序與輸入代碼中的順序一致.
因此, 如果存在一個計算任務(wù)依賴另一個計算任務(wù)的中間結(jié)果, 那么其順序性并不能靠代碼的先后順序來保證。與處理器的亂序執(zhí)行優(yōu)化類似, Java
虛擬機的即時編譯器中也有類似的指令重排序優(yōu)化.
?
我們知道, 并發(fā)編程為了保證數(shù)據(jù)的安全, 需要滿足以下三個特性
- 原子性: 指在一個操作中, CPU 不可以在中途暫停突然再調(diào)度, 即不會被中斷操作, 要不執(zhí)行完成, 要不就不執(zhí)行.
- 可見性: 指當(dāng)多個線程訪問同一個變量時, 一個線程修改了這個變量的值, 其他線程能夠立即看得到修改的值.
- 有序性: 即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行.
其實緩存一致性問題其實就是可見性問題. 而處理器優(yōu)化是可以導(dǎo)致原子性問題的, 指令重排即會導(dǎo)致有序性問題. 所以, 為了保證并發(fā)編程中可以滿足原子性, 可見性以及有序性. 就有了一個重要的概念, 那就是內(nèi)存模型.
為了保證共享內(nèi)存的正確性. 內(nèi)存模型定義了共享內(nèi)存系統(tǒng)中多線程程序讀寫操作的行為規(guī)范. 通過這些規(guī)則來規(guī)范對內(nèi)存的讀寫操作, 從而保證指令執(zhí)行的正確性. 它與處理器有關(guān), 與緩存有關(guān), 與并發(fā)有關(guān), 與編譯器也有關(guān). 它解決了 CPU 多級緩存, 處理器優(yōu)化, 指令重排等導(dǎo)致的內(nèi)存訪問問題, 保證了并發(fā)場景下的一致性, 原子性與有序性.
所以在 CPU 層面, 內(nèi)存模型定義了一個充分必要條件, 就是可見性條件.
- 有些 CPU 提供了強內(nèi)存模型, 所有 CPU 在任何時候都能看到內(nèi)存中任意位置相同的值, 這種完全是硬件提供的支持.
- 其他CPU 提供了弱內(nèi)存模型, 需要執(zhí)行一些特殊的指令(
memory barriers
內(nèi)存屏障). 刷新 CPU 緩存的數(shù)據(jù)到內(nèi)存中. 保證這個寫的操作能夠被其他 CPU 可見. 或者將 CPU 緩存的數(shù)據(jù)設(shè)置為無效狀態(tài), 保證其他 CPU 的寫操作對本 CPU 可見. 通常這些內(nèi)存屏障的行為由底層實現(xiàn), 對于上層語言的程序員來說是透明的.
?
2. Java 內(nèi)存模型是什么
上面介紹了計算機內(nèi)存模型, 這是解決多線程場景下并發(fā)問題的一個重要規(guī)范, 那么不同的編程語言, 在實現(xiàn)上也有所不同.
我們都知道 Java
程序是需要運行在 Java
虛擬機上面的, Java
內(nèi)存模型 (Java Memory Model疯淫,JMM)
就是一種符合內(nèi)存模型規(guī)范的, 屏蔽了各種硬件和操作系統(tǒng)的訪問差異的, 保證了Java
程序在各種平臺下對內(nèi)存的訪問都能保證效果一致的機制及規(guī)范.
提到 Java
內(nèi)存模型, 一般指的是 JDK 5
開始使用的新內(nèi)存模型忌卤,主要由 JSR-133:JavaTM Memory Model and Thread Specification
描述.
Java
內(nèi)存模型規(guī)定了所有的變量都存儲在主內(nèi)存中, 每個線程都有一個私有的工作內(nèi)存. 線程的工作內(nèi)存中保存了該線程中用到變量的主內(nèi)存副本拷貝, 線程對變量的所有操作都必須在工作內(nèi)存中進(jìn)行, 而不能直接讀寫主內(nèi)存. 不同線程之間也無法直接訪問對方工作內(nèi)存中的變量, 線程間變量的傳遞均需要自己的工作內(nèi)存與主內(nèi)存之間進(jìn)行數(shù)據(jù)同步.而 JMM
就作用于工作內(nèi)存與主內(nèi)存之間數(shù)據(jù)同步過程, 它規(guī)定了如何做數(shù)據(jù)同步以及什么時候做數(shù)據(jù)同步.
簡單來說: JMM 是一種規(guī)范
, 是解決由于多線程通過共享數(shù)據(jù)進(jìn)行通信時, 存在本地內(nèi)存數(shù)據(jù)不一致, 編譯器會對代碼指令重排序, 處理器對代碼亂序執(zhí)行等帶來的問題. 目的是保證并發(fā)編程場景中的原子性, 可見性和有序性.
關(guān)于主內(nèi)存與工作內(nèi)存之間的具體交互協(xié)議, JMM
定義了以下八種操作來完成. 即一個變量如何從主內(nèi)存 copy
到工作內(nèi)存, 如何從工作內(nèi)存同步到主內(nèi)存之間的細(xì)節(jié)實現(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)存變量, 它把load
操作從主內(nèi)存中得到的變量放入工作內(nèi)存的副本中. -
use
????使用: ??作用于工作內(nèi)存變量, 把工作內(nèi)存中的一個變量值傳遞給執(zhí)行引起, 每當(dāng)虛擬機遇到一個需要使用變量值的字節(jié)碼指令時執(zhí)行這個操作. -
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)存的變量中.
read
與 load
從主內(nèi)存復(fù)制變量到工作內(nèi)存
use
與 assign
執(zhí)行代碼, 修改共享變量值
store
與 write
用工作內(nèi)存變量值刷新主內(nèi)存相關(guān)內(nèi)存.
?
3. volatile
對于這個關(guān)鍵字, 相信大家都不陌生. 它就解決了上述問題中的可見性與指令重排序功能. 它可以被看做是 Java
中一種 "程度較輕的 synchronized"
volatile
關(guān)鍵字可以保證直接從主內(nèi)存中讀取一個變量, 如果這個變量被修改后, 會被強制寫回到主內(nèi)存.
?
3.1 volatile 解決的可見性問題原理
如果對聲明了 volatile
的變量進(jìn)行寫操作, JVM
就會像處理器發(fā)送一條 Lock
前綴的指令, 將這個變量所在緩存行的數(shù)據(jù)立即寫回到主內(nèi)存中. 但是就算寫回到內(nèi)存, 其他處理器的緩存的值還是舊的. 所以在多處理器下, 為了保證各個處理器的緩存是一致的, 就會實現(xiàn)緩存一致性協(xié)議. 也就每個處理器通過嗅探在總線上傳播的數(shù)據(jù)來檢查自己緩存的值是不是過期了, 當(dāng)處理器發(fā)現(xiàn)自己緩存行對應(yīng)的內(nèi)存地址被修改, 就會將當(dāng)前處理器的緩存行設(shè)置為無效狀態(tài), 當(dāng)處理器對這個數(shù)據(jù)進(jìn)行修改操作的時候, 會重新從主內(nèi)存中把數(shù)據(jù)讀到處理器緩存里.
Lock
前綴的指令在多核處理器下會引發(fā)兩件事情
- 將當(dāng)前處理器緩存行的數(shù)據(jù)寫回到主內(nèi)存
- 一個處理器的緩存回寫內(nèi)存會導(dǎo)致其他處理器的緩存無效. 需要數(shù)據(jù)操作的時候需要再次去主內(nèi)存中讀取.
對于 volatile
的緩存一致性協(xié)議 MESI
, 需要不斷的從主內(nèi)存嗅探和 cas
不斷循環(huán), 無效交互會導(dǎo)致總線帶寬達(dá)到峰值, 所以盡量不要大量的使用 volatile
.
?
3.2 volatile 的內(nèi)存語義
-
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)存中重新讀取共享變量.
- 線程 A 寫一個
volatile
修飾的變量, 實質(zhì)上是線程 A 向接下來將要讀這個volatile
修飾變量的某個線程發(fā)出了(對其共享變量所做修改)消息. - 線程 B 讀一個
volatile
修飾變量, 實質(zhì)上是線程 B 接收了某個線程發(fā)出的(在寫這個volatile
變量之前對共享變量所做修改)消息
線程 A 寫這個變量, 隨后線程 B 讀取這個變量, 這個過程實際上是線程 A 通過主內(nèi)存向 B 線程發(fā)送消息.
?
3.3 volatile 使用內(nèi)存屏障解決重排序問題
volatile
關(guān)鍵字本身就包含了禁止指令重排序的語義. 那么它是如何實現(xiàn)的呢? 就是根據(jù)我們上面說過的內(nèi)存屏障(memory barriers
). 不同的硬件平臺實現(xiàn)內(nèi)存屏障的方式也是不一樣的, Java
通過屏蔽這些差異, 統(tǒng)一由 JVM
來生成內(nèi)存屏障的指令.
重排序也可能會導(dǎo)致多線程程序出現(xiàn)的內(nèi)存可見性問題, 對于處理器重排序, JMM
的處理器重排序規(guī)則會要求 Java
編譯器在生成指令序列時插入特定類型的內(nèi)存屏障指令. 通過內(nèi)存屏障指令來禁止特定類型的處理器重排序. 通過禁止特定類型的編譯器重排序和處理器重排序, 為程序員提供一致的內(nèi)存可見性保證.
StoreLoad Barriers
是一個 全能型 的屏障, 它同時具有其他三個屏障的效果. 現(xiàn)代的多處理器大多都支持該屏障. 但是執(zhí)行該屏障開銷會很昂貴, 因為當(dāng)前處理器通常要把寫緩沖區(qū)中的數(shù)據(jù)全部刷新到內(nèi)存中.
JMM
針對編譯器制定 volatile
重排序規(guī)則表如下
- 當(dāng)?shù)谝粋€操作是
volatile
讀時, 不管第二個操作是什么, 都不能重排序. 這個規(guī)則確保volatile
讀之后的操作不會被編譯器重新排序到volatile
讀之前. - 當(dāng)?shù)谝粋€操作是
volatile
寫時, 第二個操作是volatile
讀時, 不能重排序. - 當(dāng)?shù)诙€操作是
volatile
寫時, 無論第一個操作是什么, 都不能重排序. 這個規(guī)則確保volatile
寫之前的操作不會被編譯器重排序到volatile
寫之后.
需要注意的是:volatile
寫是在前面和后面分別插入內(nèi)存屏障, 而 volatile
讀操作是在后面插入兩個內(nèi)存屏障.
下面是基于保守策略的JMM內(nèi)存屏障插入策略:
- 在每個
volatile
寫操作的前面插入一個StoreStore
屏障. - 在每個
volatile
寫操作的后面插入一個StoreLoad
屏障. - 在每個
volatile
讀操作的后面插入一個LoadLoad
屏障. - 在每個
volatile
讀操作的后面插入一個LoadStore
屏障.
從編譯器重排序規(guī)則和處理器內(nèi)存屏障插入策略來看, 只要 volatile
修飾的變量與普通變量之間的重排序可能會破壞 volatile
的內(nèi)存語義(內(nèi)存可見性), 這種重排序就會被編譯器重排序規(guī)則和處理器的內(nèi)存屏障插入策略禁止.
?
3.4 volatile 為什么只能保證單次讀寫的原子性
對于單個 volatile
變量的讀/寫具有原子性, 但是類似于復(fù)合操作, 類似于 volatile i, i++
這種就不具有原子性, 因為本質(zhì)上 i++
是三次操作. (實際上對應(yīng)的機器碼步驟更多,但是這里分解為三步已經(jīng)足夠說明問題)
int temp = i;
temp = temp + 1;
i = temp;
多線程環(huán)境, 比如 A, B 兩個線程同時執(zhí)行 i++
操作. 都執(zhí)行到了第2步, B 線程先執(zhí)行結(jié)束 i = 1
, 因為變量 i
是 volatile
修飾, 所以 B 線程執(zhí)行結(jié)束馬上刷新工作內(nèi)存中的 i = 1
到主內(nèi)存. 并且通知其他 CPU 中的線程: 主內(nèi)存中 i
的值更新了. 使 A 線程中工作內(nèi)存的 i
失效. 如果 A 線程這時候使用到變量 i
, 就需要去主內(nèi)存重新 copy
一份到自己的工作內(nèi)存.
但是這時候 A 線程執(zhí)行到了 temp = temp +1
, 已經(jīng)用臨時變量 temp
記錄了之前 i
的值, 不需要再讀去 i
的值了.
所以雖然變量 i
的值 0 在 A 線程的工作內(nèi)存中確實失效了, 但是 temp
仍然是有效的, 既然有效, 那么 A 線程將繼續(xù)將第 3 步的結(jié)果 i=1
再次寫入主內(nèi)存覆蓋了之前 B 線程寫入的值. 這就是為什么 volatile
無法保證共享變量 i++
線程安全的原因.
對于復(fù)合操作允坚,可以使用同步塊技術(shù)和 Java concurrent
包下的原子操作類等.
?
3.5 volatile 總結(jié)
- 通過使用
Lock
前綴的指令禁止變量在線程工作內(nèi)存中緩存來保證volatile
變量的內(nèi)存可見性. - 通過插入內(nèi)存屏障指令來禁止會影響變量可見性的指令重排序.
- 對任意單次
volatile
讀/寫都具有原子性, 但是對于符合操作不具有原子性.
本章到這里就結(jié)束了, 如果看完覺得對你有幫助, 還請隨手點一個贊. 謝謝大家. 你們的鼓勵就是我的動力.
下一章將會簡單學(xué)習(xí)理解 synchronized
.