一、為什麼要有記憶體模型在現代多核處理器中,每個處理器都有自己的快取,需要定期的與主記憶體進行協調。想要確保每個處理器在任意時刻知道其他處理器正在進行的工作,將需要很大的開銷,且通常是沒必要的。
1.1 硬體的效率與一致性
1、 由於計算機的儲存裝置與處理器的運算能力之間有幾個數量級的差距,所以現代計算機系統都不得不加入一層讀寫速度儘可能接近處理器運算速度的快取記憶體(cache)來作為記憶體與處理器之間的緩衝:將運算需要使用到的資料複製到快取中,讓運算能快速進行,當運算結束後再從快取同步回記憶體之中沒這樣處理器就無需等待緩慢的記憶體讀寫了。
2、多個處理器運算任務都涉及同一塊主存,需要一種協議可以保障資料的一致性,這類協議有MSI、MESI、MOSI及Dragon Protocol等。Java虛擬機器記憶體模型中定義的記憶體訪問操作與硬體的快取訪問操作是具有可比性的。
3、基於快取記憶體的儲存互動很好地解決了處理器與記憶體的速度矛盾,但是引入了一個新的問題: 快取一致性(Cache Coherence)。在多處理器系統中,每個處理器都有自己的快取記憶體,而他們又共享同一主存,下面會介紹這個問題
二、CPU和快取一致性2.1 為什麼需要CPU cache
因為CPU的頻率太快了,快到主存跟不上,這樣在處理器時鐘週期內,CPU常常需要等待主存,浪費資源。CPU往往需要重複處理相同的資料、重複執行相同的指令,如果這部分資料、指令CPU能在CPU快取中找到,CPU就不需要從記憶體或硬碟中再讀取資料、指令,從而減少了整機的響應時間,所以cache的出現,是為了緩解CPU和記憶體之間速度的不匹配問題( 結構:cpu -> cache -> memory )
在程式執行的過程中就變成了:
當程式在執行過程中,會將運算需要的資料從主存複製一份到CPU的快取記憶體當中,那麼CPU進行計算時就可以直接從它的快取記憶體讀取資料和向其中寫入資料,當運算結束之後,再將快取記憶體中的資料重新整理到主存當中。
在Intel官網上產品-處理器介面內對快取的定義為:CPU快取記憶體是處理器上的一個快速記憶區域。英特爾智慧快取記憶體(SmartCache)是指可讓所有核心動態共享最後一級快取記憶體的架構。這裡就提及到了最後一級快取記憶體的概念,即為 CPU快取中的L3(三級快取) ,那麼我們繼續來解釋一下什麼叫三級快取,分別又是指哪三級快取。
2.2 三級快取(L1、L2、L3)
1) 三級快取( L1一級快取、L2二級快取、L3三級快取 )都是整合在CPU內的快取 2) 它們的作用都是作為CPU與主記憶體之間的高速資料緩衝區 3) L1最靠近CPU核心,L2其次,L3再次 執行速度方面: L1最快、L2次快、L3最慢
容量大小方面: L1最小、L2較大、L3最大
4) CPU會先在最快的L1中尋找需要的資料,找不到再去找次快的L2,還找不到再去找L3,L3都沒有那就只能去記憶體找了。
5) 單核CPU只含有一套L1,L2,L3快取;如果CPU含有多個核心,即多核CPU,則每個核心都含有一套L1(甚至和L2)快取,而共享L3(或者和L2)快取。
單CPU雙核的快取結構:
在單執行緒環境下,cpu核心的快取只被一個執行緒訪問。快取獨佔,不會出現訪問衝突等問題 在多執行緒場景下,在CPU和主存之間增加快取,就可能存在 快取一致性問題 ,也就是說,在多核CPU中,每個核的自己的快取中,關於同一個資料的快取內容可能不一致,這也就是我們上面提到的 快取一致性的問題
2.3 亂序執行最佳化
從java原始碼到最終實際執行的指令序列,會經歷下面3種重排序:
重排序的現象:
a=10,b=a 這一組 b依賴a,不會重排序a=10,b=50 這一組 a和b 沒有關係,那麼就有可能被重排序執行 b=50,a=10 cpu和編譯器為了提高程式的執行效率會按照一定的規則允許指令最佳化,不影響單執行緒程式執行結果,但是多執行緒就會影響程式結果三、java記憶體模型Java記憶體模型即Java Memory Model,簡稱JMM。JMM定義了Java 虛擬機器(JVM)在計算機記憶體(RAM)中的工作方式。JVM是整個計算機虛擬模型,所以JMM是隸屬於JVM的。
Java記憶體模型(Java Memory Model ,JMM)就是一種符合記憶體模型規範的,遮蔽了各種硬體和作業系統的訪問差異的,保證了Java程式在各種平臺下對記憶體的訪問都能保證效果一致的機制及規範。可以避免像c++等直接使用物理硬體和作業系統的記憶體模型在不同作業系統和硬體平臺下表現不同,比如有些c/c++程式可能在windows平臺執行正常,而在linux平臺卻執行有問題。
Java執行緒之間的通訊採用的是過共享記憶體模型,這裡提到的共享記憶體模型指的就是Java記憶體模型(簡稱JMM),JMM決定一個執行緒對共享變數的寫入何時對另一個執行緒可見。從抽象的角度來看,JMM定義了執行緒和主記憶體之間的抽象關係:執行緒之間的共享變數儲存在主記憶體(main memory)中,每個執行緒都有一個私有的本地記憶體(local memory),本地記憶體中儲存了該執行緒以讀/寫共享變數的副本。本地記憶體是JMM的一個抽象概念,並不真實存在。它涵蓋了快取,寫緩衝區,暫存器以及其他的硬體和編譯器最佳化。
從上圖來看,執行緒A與執行緒B之間如要通訊的話,必須要經歷下面2個步驟:
執行緒A把本地記憶體A中更新過的共享變數重新整理到主記憶體中去。執行緒B到主記憶體中去讀取執行緒A之前已更新過的共享變數。具體示意圖:
如上圖所示,本地記憶體A和B有主記憶體中共享變數z的副本。假設初始時,這三個記憶體中的z值都為0。執行緒A在執行時,把更新後的z值(假設值為1)臨時存放在自己的本地記憶體A中。當執行緒A和執行緒B需要通訊時,執行緒A首先會把自己本地記憶體中修改後的z值重新整理到主記憶體中,此時主記憶體中的z值變為了1。隨後,執行緒B到主記憶體中去讀取執行緒A更新後的z值,此時執行緒B的本地記憶體的z值也變為了1。
從整體來看,這兩個步驟實質上是執行緒A在向執行緒B傳送訊息,而且這個通訊過程必須要經過主記憶體。JMM透過控制主記憶體與每個執行緒的本地記憶體之間的互動,來為java程式設計師提供記憶體可見性保證。
3.1 JVM對Java記憶體模型的實現
在JVM內部,Java 記憶體模型把 Java 虛擬機器內部劃分為: 執行緒棧和堆
執行緒棧:
每一個執行在 Java 虛擬機器裡的執行緒都擁有自己的執行緒棧。這個執行緒棧包含了這個執行緒呼叫的方法當前執行點相關的資訊。一個執行緒僅能訪問自己的執行緒棧。一個執行緒建立的本地變數對其它執行緒不可見,僅自己可見。即使兩個執行緒執行同樣的程式碼,這兩個執行緒仍然在在自己的執行緒棧中的程式碼來建立本地變數。因此,每個執行緒擁有每個本地變數的獨有版本。
執行緒堆:
堆上包含在 Java 程式中建立的所有物件,無論是哪一個物件建立的。這包括原始型別的物件版本。如果一個物件被建立然後賦值給一個區域性變數,或者用來作為另一個物件的成員變數,這個物件仍然是存放在堆上。
一個本地變數如果是原始型別,那麼它會被完全儲存到棧區一個本地變數也有可能是一個物件的引用,這種情況下,這個本地引用會被儲存到棧中,但是物件本身仍然儲存在堆區對於一個物件的成員方法,這些方法中包含本地變數,仍需要儲存在棧區,即使它們所屬的物件在堆區對於一個物件的成員變數,不管它是原始型別還是包裝型別,都會被儲存到堆區Static型別的變數以及類本身相關資訊都會隨著類本身儲存在堆區堆中的物件可以被多執行緒共享。如果一個執行緒獲得一個物件的應用,它便可訪問這個物件的成員變數。如果兩個執行緒同時呼叫了同一個物件的同一個方法,那麼這兩個執行緒便可同時訪問這個物件的成員變數,但是對於本地變數,每個執行緒都會複製一份到自己的執行緒棧中3.2 Java記憶體模型和硬體架構之間的橋接
Java記憶體模型和硬體記憶體架構並不一致。硬體記憶體架構中並沒有區分棧和堆,從硬體上看,不管是棧還是堆,大部分資料都會存到主存中,當然一部分棧和堆的資料也有可能會存到CPU暫存器中,如下圖所示,Java記憶體模型和計算機硬體記憶體架構是一個交叉關係:
3.3 Java記憶體模型 - 同步八種操作
1) lock(鎖定):作用於主記憶體的變數,把一個變數標識為一條執行緒獨佔狀態 2) unock(解鎖):作用於主記憶體的變數,把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖定 3) read(讀取):作用於主記憶體的變數,把一個變數值從主記憶體傳輸到執行緒的工作記憶體中,以便隨後的load動作使用 4) load(載入):作用於工作記憶體的變數,它把read操作從主記憶體中得到的變數值放入工作記憶體的變數副本中 5) use(使用):作用於工作記憶體的變數,把工作記憶體中的一個變數值傳遞給執行引擎 6) assign(賦值):作用於工作記憶體的變數,它把一個從執行引擎接收到的值賦值給工作記憶體的變數 7) store(儲存):作用於工作記憶體的變數,把工作記憶體中的一個變數的值傳送到記憶體中,以便隨後的write的操作 8) write(寫入):作用於工作記憶體的變數,它把store操作從工作記憶體中一個變數的值傳送到主記憶體的變數中
3.4 Java記憶體模型 - 同步規則
如果要把一個變數從主記憶體中複製到工作記憶體,就需要按順序地執行read和load操作,如果把變數從工作記憶體中同步回主記憶體中,就要按順序地執行store和write操作。但Java記憶體模型只要求上述操作必須按順序執行,而沒有保證必須是連續執行不允許read和load、store和write操作之一單獨出現不允許一個執行緒丟棄它的最近assign的操作,即變數在工作記憶體中改變了之後必須同步到主記憶體中不允許一個執行緒無原因的(沒有發生過任何assign操作)把資料從工作記憶體同步回主記憶體中一個新的變數只能在主記憶體中誕生,不允許在工作記憶體中直接使用一個未被初始化(load或assign)的變數。即就是對一個變數實施use和store操作之前,必須先執行過了assign和load操作一個變數在同一時刻只允許一條執行緒對其進行lock操作,但lock操作可以被同一條執行緒重複執行多次,多次執行lock後,只有執行相同次數的unlock操作,變數才會解鎖。lock和unlock必須成對出現如果對一個變數執行lock操作,將會清空工作記憶體中此變數的值,在執行引擎使用這個變數前需要重新執行load或assign操作初始化變數的值如果一個變數事先沒有被lock操作鎖定,則不允許對它執行unlock操作,也不允許去unlock一個被其他執行緒鎖定的變數對一個變數執行unlock操作之前,必須先把此變數同步到主記憶體中(執行store和write操作)原子性、可見性、有序性:可以檢視我上一篇文章:執行緒安全性詳解(原子性、可見性、有序性)
四、併發的優勢與風險優勢:1) 速度:使用處理多個請求,響應更快,複雜的操作可以分成多個程序同時執行 2) 設計:程式設計在某些情況下更簡單,也可以有更多的選擇 3) 資源利用:CPU能夠在等待IO的時候做一些其他的事情
風險:1) 安全性:多個執行緒共享資料時可能會產生與期望不相符的結果 2) 活躍性:某個操作無法繼續進行下去時,就會發生活躍性問題。比如死鎖、飢餓等問題 3) 效能:執行緒過多時會使得:CPU頻繁切換,排程時間增多;同步機制;消耗過多記憶體
五、總結CPU多級快取:快取一致性、亂序執行最佳化 Java記憶體模型:JMM規定、抽象結構、同步八種操作及規則 Java併發的優勢與風險
原文:https://mp.weixin.qq.com/s?__biz=MzI5NjY0MzEwNA==&mid=2247484831&idx=1&sn=41ae2061ac67e2d7b77cef76a3a90e4d&utm_source=tuicool&utm_medium=referral