JAVA內(nèi)存模型-高速緩存,指令重排,內(nèi)存屏障

JMM定義了Java 虛擬機(JVM)在計算機內(nèi)存(RAM)中的工作方式。JVM是整個計算機虛擬模型,所以JMM是隸屬于JVM的。從抽象的角度來看,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲在主內(nèi)存(Main Memory)中,每個線程都有一個私有的本地內(nèi)存(Local Memory),本地內(nèi)存中存儲了該線程以讀/寫共享變量的副本。本地內(nèi)存是JMM的一個抽象概念,并不真實存在。它涵蓋了緩存、寫緩沖區(qū)、寄存器以及其他的硬件和編譯器優(yōu)化。

java程序簡單調(diào)用圖

三大性質(zhì)

JMM是圍繞并發(fā)編程中原子性、可見性、有序性三個特征來建立的。

原子性
一個操作是不可中斷的,要么全部執(zhí)行成功要么全部執(zhí)行失敗,類似于事務(wù)。原子性變量操作包括read,load,assign,use,store,write。Java 基本類型的數(shù)據(jù)訪問大都是原子操作,但是long 和 double 類型是 64 位,在 32 位 JVM 中會將 64 位數(shù)據(jù)的讀寫操作分成兩次 32 位來處理,所以 long 和 double 在 32 位 JVM 中是非原子操作的。

可見性:
一個線程對主內(nèi)存的修改可以及時被其他線程觀察到。導致可見性問題的根本原因:高速緩存。

有序性
有序性指的是程序按照代碼的先后順序執(zhí)行。導致有序性的根本原因:指令重排序

線程存在本地工作內(nèi)存,線程共享的主內(nèi)存。規(guī)則:所有線程都不能直接操作主內(nèi)存,需先訪問工作內(nèi)存,再訪問主內(nèi)存。

內(nèi)存簡單訪問方式

源代碼>>>編譯器的重排序>>>CPU層面的重排序(指令級,內(nèi)存級)>>>最終執(zhí)行的指令

Java 內(nèi)存模型中的指令重排不會影響單線程的執(zhí)行順序,但是會影響多線程并發(fā)執(zhí)行的正確性,所以在并發(fā)中我們必須要想辦法保證并發(fā)代碼的有序性;在 Java 里可以通過 volatile 關(guān)鍵字保證一定的有序性,還可以通過 synchronized、Lock 來保證有序性,因為 synchronized、Lock 保證了每一時刻只有一個線程執(zhí)行同步代碼相當于單線程執(zhí)行,所以自然不會有有序性的問題;除此之外 Java 內(nèi)存模型通過 happens-before 原則如果能推導出來兩個操作的執(zhí)行順序就能先天保證有序性,否則無法保證。從 Java 內(nèi)存模型我們就能看出來多線程訪問共享變量都要經(jīng)過線程工作內(nèi)存到主存的復(fù)制和主存到線程工作內(nèi)存的復(fù)制操作,所以普通共享變量就無法保證可見性了;Java 提供了 volatile 修飾符來保證變量的可見性,每次使用 volatile 變量都會主動從主存中刷新,除此之外 synchronized、Lock、final 都可以保證變量的可見性。

CPU高速緩存

我們都知道CPU/內(nèi)存/IO三者計算速度差別很大,CPU>內(nèi)存>IO;在計算機中,cpu和內(nèi)存的交互最為頻繁,相比內(nèi)存,磁盤讀寫太慢,內(nèi)存相當于高速的緩沖區(qū)。隨著多核cpu時代的到來,內(nèi)存的讀寫速度又遠不如cpu。因此cpu上出現(xiàn)了高速緩存的概念。cpu上加入了高速緩存,用來解決處理器和內(nèi)存訪問速度差異。在多核cpu中,每個處理器都有各自的高速緩存(L1,L2,L3),而主內(nèi)存確只有一個 。大概結(jié)構(gòu)如下:

CPU緩存模型

L1是一級緩存,L1d是數(shù)據(jù)緩存,L1i是指令緩存
L2是二級緩存,比L1稍大
L3是三級緩存,L3緩存是cpu共享的高速緩存,主要目的是進一步降低內(nèi)存操作的延遲問題。

  1. CPU-01讀取數(shù)據(jù)A,數(shù)據(jù)A被讀入 CPU-01 的高速緩存中。
  2. CPU-02讀取數(shù)據(jù)A,同樣存入CPU-02高速緩存中。這樣 CPU1 , CPU2 的高速緩存擁有同樣的數(shù)據(jù)。
  3. CPU-01修改了數(shù)據(jù)A,被修改后,數(shù)據(jù)A被放回CPU-01的高速緩存行,但是還沒有寫入到主內(nèi)存 。
  4. CPU-02訪問數(shù)據(jù)A,但由于CPU-01并沒有將數(shù)據(jù)A寫入主內(nèi)存,導致了數(shù)據(jù)不一致。

高速緩存帶來了緩存不一致問題:CPU層面的解決方案:總線鎖,緩存鎖

總線鎖:處理器的鎖,鎖的是總線,鎖住之后,會導致CPU串行化,效率很慢。(CPU與其它部件通信,是通過總線的方式來通信,當線程A與主內(nèi)存通信時,先加個鎖,此時其他線程不能與主內(nèi)存通信)
緩存鎖:簡單的說,如果某個內(nèi)存區(qū)域數(shù)據(jù)A,已經(jīng)同時被兩個或以上處理器核緩存,緩存鎖就會通過緩存一致性機制阻止對其修改,以此來保證操作的原子性,當其他處理器核回寫已經(jīng)被鎖定的緩存行的數(shù)據(jù)時會導致該緩存行無效。

緩存行(Cache line):CPU緩存中的最小緩存單位
目前主流的CPU Cache的Cache Line大小都是64Bytes。假設(shè)我們有一個512字節(jié)的一級緩存,那么按照64B的緩存單位大小來算,這個一級緩存所能存放的緩存?zhèn)€數(shù)就是512/64 = 8個。

在多核CPU的情況下,每個CPU都有獨立的一級緩存,如何才能保證緩存內(nèi)部數(shù)據(jù)的一致?一致性的協(xié)議MESI。

緩存一致性協(xié)議MESI

MESI實現(xiàn)方法是在CPU緩存中保存一個標記位,以此來標記四種狀態(tài)。另外,每個CPU的緩存控制器不僅知道自己的讀寫操作,也監(jiān)聽其它CPU的讀寫操作,就是嗅探(snooping)協(xié)議。

緩存狀態(tài)

MESI 是指4種狀態(tài)的首字母。每個緩存行有4個狀態(tài),可用2個bit表示,它們分別是:

狀態(tài) 描述 監(jiān)聽任務(wù)
M 修改 (Modified) 該緩存行有效,但是數(shù)據(jù)A被修改了,和內(nèi)存中的數(shù)據(jù)A不一致,數(shù)據(jù)只存在于本CPU中。 緩存行必須時刻監(jiān)聽所有試圖讀取主存中舊數(shù)據(jù)A的操作,當數(shù)據(jù)A寫回主存并將狀態(tài)變成S(共享)狀態(tài)之后,解除該監(jiān)聽。
E 獨享(Exclusive) 該緩存行有效,數(shù)據(jù)A和內(nèi)存中的數(shù)據(jù)A一致,數(shù)據(jù)A只存在于本CPU中。 緩存行必須監(jiān)聽其它CPU讀取主存中該數(shù)據(jù)A的操作,一旦有這種操作,該緩存行需要變成S(共享)狀態(tài)。
S 共享 (Shared) 該緩存行有效,數(shù)據(jù)A和內(nèi)存中的數(shù)據(jù)A一致,數(shù)據(jù)A存在于很多CPU中。 緩存行必須監(jiān)聽其它CPU使該緩存行無效或者獨享該緩存行的請求,并將該緩存行變成無效(Invalid)。
I 無效 (Invalid) 該緩存行無效。

對于M和E狀態(tài)而言總是精確的,他們在和該緩存行的真正狀態(tài)是一致的,而S狀態(tài)可能是非一致的。如果一個緩存將處于S狀態(tài)的緩存行作廢了,而另一個緩存實際上可能已經(jīng)獨享了該緩存行,但是該緩存卻不會將該緩存行升遷為E狀態(tài),這是因為其它緩存不會廣播他們作廢掉該緩存行的通知,同樣由于緩存并沒有保存該緩存行的copy的數(shù)量,因此(即使有這種通知)也沒有辦法確定自己是否已經(jīng)獨享了該緩存行。

從上面的意義看來E狀態(tài)是一種投機性的優(yōu)化:如果一個CPU想修改一個處于S狀態(tài)的緩存行,總線事務(wù)需要將所有該緩存行的copy變成invalid狀態(tài),而修改E狀態(tài)的緩存不需要使用總線事務(wù)。

狀態(tài)轉(zhuǎn)換

MESI狀態(tài)轉(zhuǎn)換

本地:本CPU操作
遠程:其他CPU操作

本地讀:無效狀態(tài)的更新主存變成獨享或共享;其他狀態(tài)維持本狀態(tài)。
本地寫:M狀態(tài)保持不變;共享狀態(tài)、獨享狀態(tài)變M狀態(tài);無效狀態(tài)更新主存并修改變?yōu)镸修改狀態(tài)。
遠程讀:獨享變共享;共享不變;修改變共享;
遠程寫:所有狀態(tài)變無效。

MESI帶來的問題

緩存的一致性消息傳遞是耗時的,CPU切換時會產(chǎn)生延遲。當一個CPU發(fā)出緩存數(shù)據(jù)A的修改消息(緩存行狀態(tài)修改消息等)時,該CPU會等待其他緩存了該數(shù)據(jù)A的CPU響應(yīng)完成。該過程導致阻塞,阻塞會存在各種各樣的性能問題和穩(wěn)定性問題。

阻塞原因

存儲緩存-Store Bufferes

為了解決CPU狀態(tài)切換的阻塞問題,避免CPU資源的浪費,引入Store Bufferes。CPU把它想要寫入到主存的值寫到Store Bufferes,然后繼續(xù)去處理其他事情。當其他CPU都確認處理完成時,數(shù)據(jù)才會最終被提交。
看一下該過程代碼演示:

value = 3;
void cpu_01(){
  value = 10;//此時cpu_01發(fā)出消息,cpu_02變?yōu)镮狀態(tài)(store buffer 和 通知其他緩存行失效)(異步)
  isFinsh = true; //標記上一步操作發(fā)送消息完成,cpu_01修改->M狀態(tài),同步value和isFinish到主存;
}
void cpu_02(){
  if(isFinsh){
    //value一定等于10?!
    assert value == 10;
  }
}

Store Bufferes的風險:isFinsh的賦值可能在value賦值之前。
這種在可識別的行為中發(fā)生的變化稱為指令重排序(指令級別的優(yōu)化)。它只是意味著其他的CPU會讀到跟程序中寫入的順序不一樣的結(jié)果。為了解決這個問題,引入了內(nèi)存屏障。

失效隊列

緩存行狀態(tài)修改不是一個簡單的操作,它需要CPU單獨處理,另外,Store Buffers大小有限,所以CPU需要等待狀態(tài)修改確認處理完成的響應(yīng)。這兩個操作都會使得性能大幅降低。為了解決這個問題,又引入了失效隊列。

由于CPU指令優(yōu)化導致了問題,所以又提供了內(nèi)存屏障的指令,明確讓程序員告訴CPU什么地方的指令不能夠優(yōu)化

指令重排

前提:指令重排只針對單個處理器 和 編譯器的單個線程 保證響應(yīng)結(jié)果不變。

分兩個層面:編譯器和處理器的指令重排。

源代碼-》編譯器的重排序-〉CPU層面的重排序(指令級,內(nèi)存級)-》最終執(zhí)行的指令

看幾個概念

1.數(shù)據(jù)依賴性:如果兩個操作訪問同一個變量,且這兩個操作中有一個為寫操作,此時這兩個操作之間就存在數(shù)據(jù)依賴性。

說明 代碼 描述
寫后讀 a=1 ; b=a ; 寫一個變量之后,再讀該變量
寫后寫 a=1 ; a=2 ; 寫一個變量之后,再寫該變量
讀后寫 b=a ; a=1 ; 讀一個變量之后,再寫該變量

2.as-if-serial 語義:不管怎么重排序,(單處理器/單線程)執(zhí)行結(jié)果不變。編譯器和處理器都必須遵守。

為了遵守 as-if-serial 語義,編譯器和處理器不會對存在數(shù)據(jù)依賴性的操作做重排序,因為這種重排序會改變執(zhí)行結(jié)果。

程序順序規(guī)則:先行發(fā)生happens- before

重排序需要遵守happens-before規(guī)則,不能說你想怎么排就怎么排,如果那樣豈不是亂了套。

1.程序順序規(guī)則

程序順序規(guī)則中所說的每個操作happens-before于該線程中的任意后續(xù)操作,并不是說前一個操作必須要在后一個操作之前執(zhí)行,而是指前一個操作的執(zhí)行結(jié)果必須對后一個操作可見,如果不滿足這個要求那就不允許這兩個操作進行重排序。對于這一點,可能會有疑問。順序性是指,我們可以按照順序推演程序的執(zhí)行結(jié)果,但是編譯器未必一定會按照這個順序編譯,但是編譯器保證結(jié)果一定==順序推演的結(jié)果。

2.監(jiān)視器鎖規(guī)則

對一個鎖的解鎖,happens-before于隨后對這個鎖的加鎖。同一時刻只能有一個線程執(zhí)行鎖中的操作,所以鎖中的操作被重排序外界是不關(guān)心的,只要最終結(jié)果能被外界感知到就好。除了重排序,剩下影響變量可見性的就是CPU緩存了。在鎖被釋放時,A線程會把釋放鎖之前所有的操作結(jié)果同步到主內(nèi)存中,而在獲取鎖時,B線程會使自己CPU的緩存失效,重新從主內(nèi)存中讀取變量的值。這樣,A線程中的操作結(jié)果就會被B線程感知到了。

3.volatile變量規(guī)則

對一個volatile域的寫,happens-before于任意后續(xù)對這個volatile域的讀。volatile變量的操作會禁止與其它普通變量的操作進行重排序。volatile變量的寫操作就像是一條基準線,到達這條線之后,不管之前的代碼有沒有重排序,反正到達這條線之后,前面的操作都已完成并生成好結(jié)果。

4.傳遞性規(guī)則

A happens- before B;B happens- before C;==》A happens- before C;推導出

5.線程啟動規(guī)則

如果線程A執(zhí)行操作ThreadB.start()(啟動線程B),那么A線程的ThreadB.start()操作happens-before于線程B中的任意操作。線程啟動規(guī)則可以這樣去理解:調(diào)用start方法時,會將start方法之前所有操作的結(jié)果同步到主內(nèi)存中,新線程創(chuàng)建好后,需要從主內(nèi)存獲取數(shù)據(jù)。這樣在start方法調(diào)用之前的所有操作結(jié)果對于新創(chuàng)建的線程都是可見的。

6.線程結(jié)束規(guī)則

線程中的任何操作都Happens-Before其它線程檢測到該線程已經(jīng)結(jié)束。假設(shè)兩個線程s、t。在線程s中調(diào)用t.join()方法。則線程s會被掛起,等待t線程運行結(jié)束才能恢復(fù)執(zhí)行。當t.join()成功返回時,s線程就知道t線程已經(jīng)結(jié)束了。所以根據(jù)本條原則,在t線程中對共享變量的修改,對s線程都是可見的。類似的還有Thread.isAlive方法也可以檢測到一個線程是否結(jié)束??梢圆聹y,當一個線程結(jié)束時,會把自己所有操作的結(jié)果都同步到主內(nèi)存。而任何其它線程當發(fā)現(xiàn)這個線程已經(jīng)執(zhí)行結(jié)束了,就會從主內(nèi)存中重新刷新最新的變量值。所以結(jié)束的線程A對共享變量的修改,對于其它檢測了A線程是否結(jié)束的線程是可見的。

7.中斷規(guī)則

一個線程在另一個線程上調(diào)用interrupt,Happens-Before被中斷線程檢測到interrupt被調(diào)用。
假設(shè)兩個線程A和B,A先做了一些操作operationA,然后調(diào)用B線程的interrupt方法。當B線程感知到自己的中斷標識被設(shè)置時(通過拋出InterruptedException,或調(diào)用interrupted和isInterrupted),operationA中的操作結(jié)果對B都是可見的。

8.終結(jié)器規(guī)則

一個對象的構(gòu)造函數(shù)執(zhí)行結(jié)束Happens-Before它的finalize()方法的開始。
“結(jié)束”和“開始”表明在時間上,一個對象的構(gòu)造函數(shù)必須在它的finalize()方法調(diào)用時執(zhí)行完。
根據(jù)這條原則,可以確保在對象的finalize方法執(zhí)行時,該對象的所有field字段值都是可見的。

內(nèi)存屏障(Memory Barriers)

編譯器級別的內(nèi)存屏障/CPU層面的內(nèi)存屏障
CPU層面提供了三種屏障:寫屏障,讀屏障,全屏障

寫屏障Store Memory Barrier是一條告訴CPU在執(zhí)行后續(xù)指令之前,需要將該緩存行對應(yīng)的store buffer中的全部寫指令執(zhí)行完成。

讀屏障Load Memory Barrier是一條告訴CPU在執(zhí)行后續(xù)指令之前,需要將該緩存行對應(yīng)的失效隊列中的全部失效指令執(zhí)行完成。

全屏障Full Memory Barrier 是讀屏障和寫屏障的合集

void cpu_01() {
    value = 10;
    //在更新數(shù)據(jù)之前必須將所有存儲緩存(store buffer)中的指令執(zhí)行完畢。
    storeMemoryBarrier();
    finished = true;
}
void cpu_02() {
    while(!finished);
    //在讀取之前將所有失效隊列中關(guān)于該數(shù)據(jù)的指令執(zhí)行完畢。
    loadMemoryBarrier();
    assert value == 10;
}

CPU緩存淘汰策略

CPU Cache的淘汰策略。常見的淘汰策略主要有LRU和Random兩種。通常意義下LRU對于Cache的命中率會比Random更好,所以CPU Cache的淘汰策略選擇的是LRU。當然也有些實驗顯示在Cache Size較大的時候Random策略會有更高的命中率

參考

CPU緩存一致性協(xié)議MESI
指令重排序

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容