1. 程式人生 > >淺談快取一致性原則和Java記憶體模型(JMM)

淺談快取一致性原則和Java記憶體模型(JMM)

Java記憶體模型(JMM)是一個概念模型,底層是計算機的暫存器、快取記憶體、主記憶體和CPU等。
多處理器環境下,共享資料的互動硬體裝置之間的關係:
這裡寫圖片描述
JMM:
這裡寫圖片描述
從以上兩張圖中,談一談以下幾個概念:

1.快取一致性協議(MESI):

由於每個處理器都含有私有的快取記憶體,在對快取中資料進行更新後,其他處理器中所含有的該共享變數的快取如果被處理器進行讀操作,就會出現錯誤。有些計算機採用LOCK#訊號對匯流排進行鎖定,當一個處理器在總線上輸出此訊號時,其它處理器的請求將被阻塞,那麼該處理器就能獨自共享記憶體。然而匯流排鎖定的開銷太大,在之後的計算機中一般都採用“快取鎖定”的方式實現。
MESI是代表了快取資料的四種狀態的首字母,分別是Modified、Exclusive、Shared、Invalid)
- M(Modified):被修改的。處於這一狀態的資料,只在本CPU中有快取資料,而其他CPU中沒有。同時其狀態相對於記憶體中的值來說,是已經被修改的,且沒有更新到記憶體中。
- E(Exclusive):獨佔的。處於這一狀態的資料,只有在本CPU中有快取,且其資料沒有修改,即與記憶體中一致。
- S(Shared):共享的。處於這一狀態的資料在多個CPU中都有快取,且與記憶體一致。
- I(Invalid):要麼已經不在快取中,要麼它的內容已經過時。為了達到快取的目的,這種狀態的段將會被忽略。一旦快取段被標記為失效,那效果就等同於它從來沒被載入到快取中。
在快取行中有這四種狀態的基礎上,通過“嗅探”

技術完成以下功能:【嗅探技術能夠嗅探其他處理器訪問主記憶體和它們的內部快取】
- 一個處於M狀態的快取行,必須時刻監聽所有試圖讀取該快取行對應的主存地址的操作,如果監聽到,則必須在此操作執行前把其快取行中的資料寫回CPU。
- 一個處於S狀態的快取行,必須時刻監聽使該快取行無效或者獨享該快取行的請求,如果監聽到,則必須把其快取行狀態設定為I。
- 一個處於E狀態的快取行,必須時刻監聽其他試圖讀取該快取行對應的主存地址的操作,如果監聽到,則必須把其快取行狀態設定為S。
- 只有E和M可以進行寫操作而且不需要額外操作,如果想對S狀態的快取欄位進行寫操作,那必須先發送一個RFO(Request-For-Ownership)廣播,該廣播可以讓其他CPU的快取中的相同資料的欄位實效,即變成I狀態。
通過以上機制可以使得處理器在每次讀寫操作都是原子的,並且每次讀到的資料都是最新的。

2.Java併發程式設計中要保證的幾個原則

1)原子性:

是指CPU在執行操作時,要麼執行要麼不執行,對於單個的讀/寫操作,在多執行緒環境下保證是原子操作,但複合操作比如i++,相當於是以下三個操作:

int temp = get();  // 讀
temp += 1;         // ADD
set(temp);         // 寫

Java主要提供了鎖機制以及CAS操作實現原子性,對於單個讀/寫操作是通過LOCK#訊號或“快取鎖定”實現的。
除此之外,long和double型別的變數讀/寫是非原子性的,每次都只讀/寫32位資料,所以一個單個的讀/寫操作就變成了兩個讀/寫操作,有可能在只讀/寫了其中32位操作後CPU就被其他執行緒搶佔到。

2)可見性:

由於每個執行緒都有一個私有的工作空間,並且儲存一個主存中共享變數的副本,線上程對私有的工作空間中的資料進行寫操作,別的執行緒並沒有讀到最新的值,就會出現問題。Java提供了volatile關鍵字保證了記憶體的可見性,底層通過LOCK#或“快取鎖定”實現。

instance = new Singleton();     // instance 是一個volatile變數

以上程式碼在進行反彙編後得到的彙編程式碼如下:

0x01a3de1d: movb $0×0,0×1104800(%esi);
0x01a3de24: lock addl $0×0,(%esp);

如果是一個volatile關鍵字修飾的變數,則會有第二行的彙編程式碼,這是一條含有lock字首的程式碼。帶有lock字首的程式碼則會通過LOCK#或通過“快取鎖定”實現執行緒間的可見性。

3)有序性

編譯器和處理器會通過多種方式比如重排序對程式碼進行優化,然而在重排序後可能會導致執行結果與預想的不同。
a.重排序的方式:
計算機在執行程式時,為了提高效能,編譯器和處理器的常常會對指令做重排,一般分以下3種:
這裡寫圖片描述
- 編譯器優化的重排序:
編譯器在不改變單執行緒程式語義的前提下,可以重新安排語句的執行順序。【as-if-serial原則保證,as-if-serial語義:不管怎麼重排序(編譯器和處理器為了提高並行度),(單執行緒)程式的執行結果不能被改變。】
- 指令級並行的重排序:
現代處理器採用了指令級並行技術來將多條指令重疊執行。如果不存在資料依賴性(即後一個執行的語句無需依賴前面執行的語句的結果),處理器可以改變語句對應的機器指令的執行順序。
- 記憶體系統重排序:
由於處理器使用快取和讀寫快取衝區,這使得載入(load)和儲存(store)操作看上去可能是在亂序執行,因為三級快取的存在,導致記憶體與快取的資料同步存在時間差。

b.記憶體屏障(Memory Barrier,又稱記憶體柵欄):
記憶體屏障是一個CPU指令,Java編譯器在生成指令序列的適當位置會插入記憶體屏障指令來禁止特定型別的處理器重排序。它的作用有兩個:
一是保證特定操作的執行順序;
二是保證某些變數的記憶體可見性。
如果在指令間插入一條Memory Barrier則會告訴編譯器和CPU,不管什麼指令都不能和這條Memory Barrier指令重排序,也就是說通過插入記憶體屏障禁止在記憶體屏障前後的指令執行重排序優化。Memory Barrier的另外一個作用是強制刷出各種CPU的快取資料,因此任何CPU上的執行緒都能讀取到這些資料的最新版本。
JMM把記憶體屏障指令分為下列四類:
這裡寫圖片描述

3.happens-before原則:

使用happens-before的概念來指定兩個操作之間的執行順序。由於這兩個操作可以在一個執行緒之內,也可以是在不同執行緒之間。因此,JMM可以通過happens-before關係向程式設計師提供跨執行緒的記憶體可見性保證。
【如果A happens-before B,JMM並不要求A一定要在B之前執行。JMM僅僅要求A操作(執行的結果)對B操作可見,且A操作按順序排在B操作之前。】
1)程式順序規則:一個執行緒中的每個操作,happens-before於該執行緒中的任意後續操作。【在A happens-before B中,如果A和B重排序後不會導致結果變化,那麼這種重排序是被允許的】
2)監視器鎖規則:對一個鎖的解鎖,happens-before於隨後對這個鎖的加鎖。
3)volatile變數規則:對一個volatile域的寫,happens-before於任意後續對這個volatile域的讀。
4)傳遞性:如果A happens-before B,且B happens-before C,那麼A happens-before C。
5)start()規則:如果執行緒A執行操作ThreadB.start()(啟動執行緒B),那麼A執行緒的ThreadB.start()操作happens-before於執行緒B中的任意操作。
6)join()規則:如果執行緒A執行操作ThreadB.join()併成功返回,那麼執行緒B中的任意操作
happens-before於執行緒A從ThreadB.join()操作成功返回。