JMM定義了Java 虛擬機(JVM)在計算機內存(RAM)中的工作方式野哭。JVM是整個計算機虛擬模型,所以JMM是隸屬于JVM的幻件。從抽象的角度來看拨黔,JMM定義了線程和主內存之間的抽象關系:線程之間的共享變量存儲在主內存(Main Memory)中,每個線程都有一個私有的本地內存(Local Memory)绰沥,本地內存中存儲了該線程以讀/寫共享變量的副本篱蝇。本地內存是JMM的一個抽象概念,并不真實存在徽曲。它涵蓋了緩存零截、寫緩沖區(qū)、寄存器以及其他的硬件和編譯器優(yōu)化秃臣。
三大性質
JMM是圍繞并發(fā)編程中原子性涧衙、可見性、有序性三個特征來建立的。
原子性
一個操作是不可中斷的绍撞,要么全部執(zhí)行成功要么全部執(zhí)行失敗正勒,類似于事務。原子性變量操作包括read傻铣,load章贞,assign,use非洲,store鸭限,write。Java 基本類型的數據訪問大都是原子操作两踏,但是long 和 double 類型是 64 位败京,在 32 位 JVM 中會將 64 位數據的讀寫操作分成兩次 32 位來處理,所以 long 和 double 在 32 位 JVM 中是非原子操作的梦染。可見性:
一個線程對主內存的修改可以及時被其他線程觀察到赡麦。導致可見性問題的根本原因:高速緩存。有序性
有序性指的是程序按照代碼的先后順序執(zhí)行帕识。導致有序性的根本原因:指令重排序
線程存在本地工作內存泛粹,線程共享的主內存。規(guī)則:所有線程都不能直接操作主內存肮疗,需先訪問工作內存晶姊,再訪問主內存。
源代碼>>>編譯器的重排序>>>CPU層面的重排序(指令級伪货,內存級)>>>最終執(zhí)行的指令
Java 內存模型中的指令重排不會影響單線程的執(zhí)行順序们衙,但是會影響多線程并發(fā)執(zhí)行的正確性,所以在并發(fā)中我們必須要想辦法保證并發(fā)代碼的有序性碱呼;在 Java 里可以通過 volatile 關鍵字保證一定的有序性蒙挑,還可以通過 synchronized、Lock 來保證有序性巍举,因為 synchronized脆荷、Lock 保證了每一時刻只有一個線程執(zhí)行同步代碼相當于單線程執(zhí)行,所以自然不會有有序性的問題懊悯;除此之外 Java 內存模型通過 happens-before 原則如果能推導出來兩個操作的執(zhí)行順序就能先天保證有序性,否則無法保證梦皮。從 Java 內存模型我們就能看出來多線程訪問共享變量都要經過線程工作內存到主存的復制和主存到線程工作內存的復制操作炭分,所以普通共享變量就無法保證可見性了;Java 提供了 volatile 修飾符來保證變量的可見性剑肯,每次使用 volatile 變量都會主動從主存中刷新捧毛,除此之外 synchronized、Lock、final 都可以保證變量的可見性呀忧。
CPU高速緩存
我們都知道CPU/內存/IO三者計算速度差別很大师痕,CPU>內存>IO;在計算機中,cpu和內存的交互最為頻繁而账,相比內存胰坟,磁盤讀寫太慢,內存相當于高速的緩沖區(qū)泞辐。隨著多核cpu時代的到來笔横,內存的讀寫速度又遠不如cpu。因此cpu上出現(xiàn)了高速緩存的概念咐吼。cpu上加入了高速緩存吹缔,用來解決處理器和內存訪問速度差異。在多核cpu中锯茄,每個處理器都有各自的高速緩存(L1,L2,L3)厢塘,而主內存確只有一個 。大概結構如下:
L1是一級緩存肌幽,L1d是數據緩存俗冻,L1i是指令緩存
L2是二級緩存,比L1稍大
L3是三級緩存牍颈,L3緩存是cpu共享的高速緩存迄薄,主要目的是進一步降低內存操作的延遲問題。
- CPU-01讀取數據A煮岁,數據A被讀入 CPU-01 的高速緩存中讥蔽。
- CPU-02讀取數據A,同樣存入CPU-02高速緩存中画机。這樣 CPU1 冶伞, CPU2 的高速緩存擁有同樣的數據。
- CPU-01修改了數據A步氏,被修改后响禽,數據A被放回CPU-01的高速緩存行,但是還沒有寫入到主內存 荚醒。
- CPU-02訪問數據A芋类,但由于CPU-01并沒有將數據A寫入主內存,導致了數據不一致界阁。
高速緩存帶來了緩存不一致問題:CPU層面的解決方案:總線鎖侯繁,緩存鎖
總線鎖:處理器的鎖,鎖的是總線泡躯,鎖住之后贮竟,會導致CPU串行化丽焊,效率很慢。(CPU與其它部件通信咕别,是通過總線的方式來通信技健,當線程A與主內存通信時,先加個鎖惰拱,此時其他線程不能與主內存通信)
緩存鎖:簡單的說雌贱,如果某個內存區(qū)域數據A,已經同時被兩個或以上處理器核緩存弓颈,緩存鎖就會通過緩存一致性機制阻止對其修改帽芽,以此來保證操作的原子性,當其他處理器核回寫已經被鎖定的緩存行的數據時會導致該緩存行無效翔冀。
緩存行(Cache line):CPU緩存中的最小緩存單位
目前主流的CPU Cache的Cache Line大小都是64Bytes导街。假設我們有一個512字節(jié)的一級緩存,那么按照64B的緩存單位大小來算纤子,這個一級緩存所能存放的緩存?zhèn)€數就是512/64 = 8個搬瑰。
在多核CPU的情況下,每個CPU都有獨立的一級緩存控硼,如何才能保證緩存內部數據的一致泽论?一致性的協(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)聽任務 |
---|---|---|
M 修改 (Modified) | 該緩存行有效锁施,但是數據A被修改了陪踩,和內存中的數據A不一致,數據只存在于本CPU中悉抵。 | 緩存行必須時刻監(jiān)聽所有試圖讀取主存中舊數據A的操作肩狂,當數據A寫回主存并將狀態(tài)變成S(共享)狀態(tài)之后,解除該監(jiān)聽基跑。 |
E 獨享(Exclusive) | 該緩存行有效婚温,數據A和內存中的數據A一致,數據A只存在于本CPU中媳否。 | 緩存行必須監(jiān)聽其它CPU讀取主存中該數據A的操作栅螟,一旦有這種操作,該緩存行需要變成S(共享)狀態(tài)篱竭。 |
S 共享 (Shared) | 該緩存行有效力图,數據A和內存中的數據A一致,數據A存在于很多CPU中掺逼。 | 緩存行必須監(jiān)聽其它CPU使該緩存行無效或者獨享該緩存行的請求吃媒,并將該緩存行變成無效(Invalid)。 |
I 無效 (Invalid) | 該緩存行無效吕喘。 | 無 |
對于M和E狀態(tài)而言總是精確的赘那,他們在和該緩存行的真正狀態(tài)是一致的,而S狀態(tài)可能是非一致的氯质。如果一個緩存將處于S狀態(tài)的緩存行作廢了募舟,而另一個緩存實際上可能已經獨享了該緩存行,但是該緩存卻不會將該緩存行升遷為E狀態(tài)闻察,這是因為其它緩存不會廣播他們作廢掉該緩存行的通知拱礁,同樣由于緩存并沒有保存該緩存行的copy的數量,因此(即使有這種通知)也沒有辦法確定自己是否已經獨享了該緩存行辕漂。
從上面的意義看來E狀態(tài)是一種投機性的優(yōu)化:如果一個CPU想修改一個處于S狀態(tài)的緩存行呢灶,總線事務需要將所有該緩存行的copy變成invalid狀態(tài),而修改E狀態(tài)的緩存不需要使用總線事務钉嘹。
狀態(tài)轉換
本地:本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切換時會產生延遲鹏浅。當一個CPU發(fā)出緩存數據A的修改消息(緩存行狀態(tài)修改消息等)時嗅义,該CPU會等待其他緩存了該數據A的CPU響應完成。該過程導致阻塞隐砸,阻塞會存在各種各樣的性能問題和穩(wěn)定性問題之碗。
存儲緩存-Store Bufferes
為了解決CPU狀態(tài)切換的阻塞問題,避免CPU資源的浪費季希,引入Store Bufferes褪那。CPU把它想要寫入到主存的值寫到Store Bufferes幽纷,然后繼續(xù)去處理其他事情。當其他CPU都確認處理完成時博敬,數據才會最終被提交友浸。
看一下該過程代碼演示:
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會讀到跟程序中寫入的順序不一樣的結果驮肉。為了解決這個問題,引入了內存屏障括勺。
失效隊列
緩存行狀態(tài)修改不是一個簡單的操作缆八,它需要CPU單獨處理,另外疾捍,Store Buffers大小有限奈辰,所以CPU需要等待狀態(tài)修改確認處理完成的響應。這兩個操作都會使得性能大幅降低乱豆。為了解決這個問題奖恰,又引入了失效隊列。
由于CPU指令優(yōu)化導致了問題宛裕,所以又提供了內存屏障的指令瑟啃,明確讓程序員告訴CPU什么地方的指令不能夠優(yōu)化
指令重排
前提:指令重排只針對單個處理器 和 編譯器的單個線程 保證響應結果不變。
分兩個層面:編譯器和處理器的指令重排揩尸。
源代碼-》編譯器的重排序-〉CPU層面的重排序(指令級蛹屿,內存級)-》最終執(zhí)行的指令
看幾個概念
1.數據依賴性:如果兩個操作訪問同一個變量,且這兩個操作中有一個為寫操作岩榆,此時這兩個操作之間就存在數據依賴性错负。
說明 代碼 描述 寫后讀 a=1 ; b=a ; 寫一個變量之后,再讀該變量 寫后寫 a=1 ; a=2 ; 寫一個變量之后勇边,再寫該變量 讀后寫 b=a ; a=1 ; 讀一個變量之后犹撒,再寫該變量 2.as-if-serial 語義:不管怎么重排序,(單處理器/單線程)執(zhí)行結果不變粒褒。編譯器和處理器都必須遵守识颊。
為了遵守 as-if-serial 語義,編譯器和處理器不會對存在數據依賴性的操作做重排序奕坟,因為這種重排序會改變執(zhí)行結果祥款。
程序順序規(guī)則:先行發(fā)生happens- before
重排序需要遵守happens-before規(guī)則清笨,不能說你想怎么排就怎么排,如果那樣豈不是亂了套镰踏。
1.程序順序規(guī)則
程序順序規(guī)則中所說的每個操作happens-before于該線程中的任意后續(xù)操作函筋,并不是說前一個操作必須要在后一個操作之前執(zhí)行沙合,而是指前一個操作的執(zhí)行結果必須對后一個操作可見奠伪,如果不滿足這個要求那就不允許這兩個操作進行重排序。對于這一點首懈,可能會有疑問绊率。順序性是指,我們可以按照順序推演程序的執(zhí)行結果究履,但是編譯器未必一定會按照這個順序編譯滤否,但是編譯器保證結果一定==順序推演的結果。
2.監(jiān)視器鎖規(guī)則
對一個鎖的解鎖最仑,happens-before于隨后對這個鎖的加鎖藐俺。同一時刻只能有一個線程執(zhí)行鎖中的操作,所以鎖中的操作被重排序外界是不關心的泥彤,只要最終結果能被外界感知到就好欲芹。除了重排序,剩下影響變量可見性的就是CPU緩存了吟吝。在鎖被釋放時菱父,A線程會把釋放鎖之前所有的操作結果同步到主內存中,而在獲取鎖時剑逃,B線程會使自己CPU的緩存失效浙宜,重新從主內存中讀取變量的值。這樣蛹磺,A線程中的操作結果就會被B線程感知到了粟瞬。
3.volatile變量規(guī)則
對一個volatile域的寫,happens-before于任意后續(xù)對這個volatile域的讀萤捆。volatile變量的操作會禁止與其它普通變量的操作進行重排序裙品。volatile變量的寫操作就像是一條基準線,到達這條線之后鳖轰,不管之前的代碼有沒有重排序清酥,反正到達這條線之后,前面的操作都已完成并生成好結果蕴侣。
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ī)則可以這樣去理解:調用start方法時,會將start方法之前所有操作的結果同步到主內存中揩懒,新線程創(chuàng)建好后什乙,需要從主內存獲取數據。這樣在start方法調用之前的所有操作結果對于新創(chuàng)建的線程都是可見的已球。
6.線程結束規(guī)則
線程中的任何操作都Happens-Before其它線程檢測到該線程已經結束臣镣。假設兩個線程s、t智亮。在線程s中調用t.join()方法忆某。則線程s會被掛起,等待t線程運行結束才能恢復執(zhí)行阔蛉。當t.join()成功返回時弃舒,s線程就知道t線程已經結束了翰蠢。所以根據本條原則杖剪,在t線程中對共享變量的修改,對s線程都是可見的触机。類似的還有Thread.isAlive方法也可以檢測到一個線程是否結束颠区∠髅蹋可以猜測,當一個線程結束時瓦呼,會把自己所有操作的結果都同步到主內存喂窟。而任何其它線程當發(fā)現(xiàn)這個線程已經執(zhí)行結束了,就會從主內存中重新刷新最新的變量值央串。所以結束的線程A對共享變量的修改磨澡,對于其它檢測了A線程是否結束的線程是可見的。
7.中斷規(guī)則
一個線程在另一個線程上調用interrupt,Happens-Before被中斷線程檢測到interrupt被調用质和。
假設兩個線程A和B稳摄,A先做了一些操作operationA,然后調用B線程的interrupt方法饲宿。當B線程感知到自己的中斷標識被設置時(通過拋出InterruptedException厦酬,或調用interrupted和isInterrupted),operationA中的操作結果對B都是可見的。
8.終結器規(guī)則
一個對象的構造函數執(zhí)行結束Happens-Before它的finalize()方法的開始瘫想。
“結束”和“開始”表明在時間上仗阅,一個對象的構造函數必須在它的finalize()方法調用時執(zhí)行完。
根據這條原則国夜,可以確保在對象的finalize方法執(zhí)行時减噪,該對象的所有field字段值都是可見的。
內存屏障(Memory Barriers)
編譯器級別的內存屏障/CPU層面的內存屏障
CPU層面提供了三種屏障:寫屏障,讀屏障筹裕,全屏障
寫屏障Store Memory Barrier是一條告訴CPU在執(zhí)行后續(xù)指令之前醋闭,需要將該緩存行對應的store buffer中的全部寫指令執(zhí)行完成。
讀屏障Load Memory Barrier是一條告訴CPU在執(zhí)行后續(xù)指令之前朝卒,需要將該緩存行對應的失效隊列中的全部失效指令執(zhí)行完成证逻。
全屏障Full Memory Barrier 是讀屏障和寫屏障的合集
void cpu_01() {
value = 10;
//在更新數據之前必須將所有存儲緩存(store buffer)中的指令執(zhí)行完畢。
storeMemoryBarrier();
finished = true;
}
void cpu_02() {
while(!finished);
//在讀取之前將所有失效隊列中關于該數據的指令執(zhí)行完畢抗斤。
loadMemoryBarrier();
assert value == 10;
}
CPU緩存淘汰策略
CPU Cache的淘汰策略囚企。常見的淘汰策略主要有LRU和Random兩種。通常意義下LRU對于Cache的命中率會比Random更好豪治,所以CPU Cache的淘汰策略選擇的是LRU洞拨。當然也有些實驗顯示在Cache Size較大的時候Random策略會有更高的命中率