啃碎併發(10):內存模型以內部原理

前言


如上一篇文章所述,Java內存模型規範了Java虛擬機與計算機內存是如何協同工做的。Java虛擬機是一個完整計算機的模型,所以,這個模型天然會包含一個內存模型—又稱爲Java內存模型。程序員

若是你想設計表現良好的併發程序,理解Java內存模型是很是重要的。Java內存模型規定了如何和什麼時候能夠看到由其餘線程修改事後的共享變量的值,以及在必須時如何同步的訪問共享變量shell

1 Java內存模型

咱們先來看看Java 線程運行內存示意圖,以下圖所示:數據庫

Java 線程運行內存示意圖數組

這張圖告訴咱們在線程運行的時候有一個內存專用的一小塊內存,當Java程序會將變量同步到線程所在的內存,這時候會操做工做內存中的變量,而線程中變量的值什麼時候同步回主內存是不可預期的緩存

所以,依據上面圖的線程運行內存示意圖,Java內存模型在JVM內部抽象劃分爲線程棧和堆。以下圖所示安全

JMM劃分爲線程棧和堆服務器

1.1 線程棧與堆

每個運行在Java虛擬機裏的線程都擁有本身的線程棧。這個線程棧包含了 線程調用的方法當前執行點相關的信息,同時線程棧具備以下特性:網絡

即便兩個線程執行一樣的代碼,這兩個線程任然在在本身的線程棧中的代碼來建立本地變量。所以,每一個線程擁有每一個本地變量的獨有版本多線程

全部原始類型的本地變量都存放在線程棧上,所以對其它線程不可見。一個線程可能向另外一個線程傳遞一個原始類型變量的拷貝,可是它不能共享這個原始類型變量自身。架構

堆上包含在Java程序中建立的全部對象,不管是哪個對象建立的。這包括原始類型的對象版本。若是一個對象被建立而後賦值給一個局部變量,或者用來做爲另外一個對象的成員變量,這個對象任然是存放在堆上

因此,調用棧和本地變量存放在線程棧上,對象存放在堆上,以下圖所示:

線程棧與堆 & 變量、對象、調用棧

存放在堆上的對象能夠被全部持有對這個對象引用的線程訪問。當一個線程能夠訪問一個對象時,它也能夠訪問這個對象的成員變量。若是兩個線程同時調用同一個對象上的同一個方法,它們將會都訪問這個對象的成員變量,可是每個線程都擁有這個本地變量的私有拷貝。

上面說到的幾點,以下圖所示:

棧、堆 & 本地變量、靜態變量

1.2 CPU與內存

衆所周知,CPU是計算機的大腦,它負責執行程序的指令。內存負責存數據,包括程序自身數據。一樣你們都知道,內存比CPU慢不少,如今獲取內存中的一條數據大概須要200多個CPU週期(CPU cycles),而CPU寄存器通常狀況下1個CPU週期就夠了。下面是CPU Cache的簡單示意圖:

CPU Cache示意圖

隨着多核的發展,CPU Cache分紅了三個級別:L1,L2,L3。級別越小越接近CPU,因此速度也更快,同時也表明着容量越小。

在Linux下面用 cat /proc/cpuinfo,或Ubuntu下 lscpu 看看本身機器的緩存狀況,更細的能夠經過如下命令看看:

就像數據庫cache同樣,獲取數據時首先會在最快的cache中找數據,若是沒有命中(Cache miss) 則往下一級找,直到三層Cache都找不到,那隻要向內存要數據了。一次次地未命中,表明獲取數據消耗的時間越長。

同時,爲了高效地存取緩存,不是簡單隨意地將單條數據寫入緩存的。緩存是由緩存行組成的,典型的一行是64字節。能夠經過下面的shell命令,查看cherency_line_size就知道知道機器的緩存行是多大:

CPU存取緩存都是以「行」爲最小單位操做的。好比:一個Java long型佔8字節,因此從一條緩存行上你能夠獲取到8個long型變量。因此若是你訪問一個long型數組,當有一個long被加載到cache中, 你將無消耗地加載了另外7個。因此你能夠很是快地遍歷數組。

2 緩存一致性

因爲CPU和主存的處理速度上存在必定差異,爲了匹配這種差距,提高計算機能力,人們在CPU和主存之間增長了多層高速緩存。每一個CPU會有L一、L2甚至L3緩存,在多核計算機中會有多個CPU,那麼就會存在多套緩存,在這多套緩存之間的數據就可能出現不一致的現象。爲了解決這個問題,有了內存模型。內存模型定義了共享內存系統中多線程程序讀寫操做行爲的規範。經過這些規則來規範對內存的讀寫操做,從而保證指令執行的正確性。

其實Java內存模型告訴咱們經過使用關鍵詞「synchronized」或「volatile」可讓Java保證某些約束:

經過以上描述咱們就能夠寫出線程安全的Java程序,JDK也同時幫咱們屏蔽了不少底層的東西。

因此,在編譯器各類優化及多種類型的微架構平臺上,Java語言規範制定者試圖建立一個虛擬的概念並傳遞到Java程序員,讓他們可以在這個虛擬的概念上寫出線程安全的程序來,而編譯器實現者會根據Java語言規範中的各類約束在不一樣的平臺上達到Java程序員所須要的線程安全這個目的

那麼,在多種類型微架構平臺上,又是如何解決緩存不一致性問題的呢?這是衆多CPU廠商必須解決的問題。爲了解決前面提到的緩存數據不一致的問題,人們提出過不少方案,一般來講有如下2種方案:

2.1 總線的概念

首先,上面的兩種方案,其實都涉及到了總線的概念,那到底什麼是總線呢?總線是處理器與主存以及處理器與處理器之間進行通訊的媒介,有兩種基本的互聯結構:SMP(symmetric multiprocessing 對稱多處理)和NUMA(nonuniform memory access 非一致內存訪問)

SMP(對稱多處理)和NUMA(非一致內存訪問)

SMP系統結構很是普通,由於它們最容易構建,不少小型服務器採用這種結構。處理器和存儲器之間採用總線互聯,處理器和存儲器都有負責發送和監聽總線廣播的信息的總線控制單元。可是同一時刻只能有一個處理器(或存儲控制器)在總線上廣播,全部的處理器均可以監聽。很容易看出,對總線的使用是SMP結構的瓶頸。

NUMP系統結構中,一系列節點經過點對點網絡互聯,像一個小型互聯網,每一個節點包含一個或多個處理器和一個本地存儲器。一個節點的本地存儲對於其餘節點是可見的,全部節點的本地存儲一塊兒造成了一個能夠被全部處理器共享的全局存儲器。能夠看出,NUMP的本地存儲是共享的,而不是私有的,這點和SMP是不一樣的。NUMP的問題是網絡比總線複製,須要更加複雜的協議,處理器訪問本身節點的存儲器速度快於訪問其餘節點的存儲器。NUMP的擴展性很好,因此目前不少大中型的服務器在採用NUMP結構

對於上層程序員來講,最須要理解的是總線線是一種重要的資源,使用的好壞會直接影響程序的執行性能

2.2 總線加Lock

在早期的CPU當中,是能夠經過在總線上加LOCK#鎖的形式來解決緩存不一致的問題。由於CPU和其餘部件進行通訊都是經過總線來進行的,若是對總線加LOCK#鎖的話,也就是說阻塞了其餘CPU對其餘部件訪問(如內存),從而使得只能有一個CPU能使用這個變量的內存。在總線上發出了LCOK#鎖的信號,那麼只有等待這段代碼徹底執行完畢以後,其餘CPU才能從其內存讀取變量,而後進行相應的操做。這樣就解決了緩存不一致的問題。

可是因爲在鎖住總線期間,其餘CPU沒法訪問內存,會致使效率低下。所以出現了第二種解決方案,經過緩存一致性協議來解決緩存一致性問題。

2.3 緩存一致性協議

一致性要求是指,若cache中某個字段被修改,那麼在主存(以及更高層次)上,該字段的副本必須當即或最後加以修改,並確保它者引用主存上該字內容的正確性。

當代多處理器系統中,每一個處理器大都有本身的cache。同一主存塊的拷貝能同時存於不一樣cache中,若容許處理器各自獨立地修改本身的cache,就會出現不一致問題。解決此問題有軟件辦法和硬件辦法。硬件辦法能動態地識別出不一致產生的條件並予以及時處理,從而使cache的使用有很高的效率。而且此辦法對程序員和系統軟件開發人員是透明的,減輕了軟件研製負擔,從而廣泛被採用。

軟件辦法最出名的就是Intel 的MESI協議,MESI協議保證了每一個緩存中使用的共享變量的副本是一致的。MESI協議是一種採用寫--無效方式的監聽協議。它要求每一個cache行有兩個狀態位,用於描述該行當前是處於修改態(M)、專有態(E)、共享態(S)或者無效態(I)中的哪一種狀態,從而決定它的讀/寫操做行爲。這四種狀態的定義是:

MESI協議適合以總線爲互連機構的多處理器系統各cache控制器除負責響應本身CPU的內存讀寫操做(包括讀/寫命中與未命中)外,還要負責監聽總線上的其它CPU的內存讀寫活動(包括讀監聽命中與寫監聽命中)並對本身的cache予以相應處理全部這些處理過程要維護cache一致性,必須符合MESI協議狀態轉換規則

MESI的總線監聽與狀態轉換

下面由圖的四個頂點出發,介紹總線監聽與狀態轉換規則:

上述分析能夠看出,雖然各cache控制器隨時都在監聽系統總線,但能監聽到的只有讀未命中、寫未命中以及共享行寫命中三種狀況。總線監控邏輯並不複雜,增添的系統總線傳輸開銷也不大,MESI協議卻有力地保證了主存塊髒拷貝在多cache中的惟一性,並能及時寫回,保證cache主存存取的正確性。

可是,值得注意的是,傳統的MESI協議中有兩個行爲的執行成本比較大。一個是將某個Cache Line標記爲Invalid狀態,另外一個是當某Cache Line當前狀態爲Invalid時寫入新的數據。因此CPU經過Store Buffer和Invalidate Queue組件來下降這類操做的延時。以下圖所示:

CPU經過Store Buffer和Invalidate Queue組件來下降這類操做的延時

因此,MESI協議,能夠保證緩存的一致性,可是沒法保證明時性,可能會有極短期的髒讀問題

其實,並不是全部狀況都會使用緩存一致性的,如:被操做的數據不能被緩存在CPU內部或操做數據跨越多個緩存行(狀態沒法標識),則處理器會調用總線鎖定;另外當CPU不支持緩存鎖定時,天然也只能用總線鎖定了,好比說奔騰486以及更老的CPU。

相關文章
相關標籤/搜索