下圖簡單的展現了最簡單的高速緩存的配置,數據的讀取和存儲都通過高速緩存,CPU核心與高速緩存有一條特殊的快速通道;主存與高速緩存都連在系統總線上(BUS)這條總線同時還用於其餘組件的通訊:
java
在高速緩存出現後不久,系統變得愈來愈複雜,高速緩存與主存之間的速度差別被拉大,直到加入了另外一級緩存,新加入的這級緩存比第一緩存更大,可是更慢,並且經濟上不合適,因此有了二級緩存,甚至有些系統已經擁有了三級緩存,因而就演變成了多級緩存,以下圖:
緩存
爲何須要CPU cache:安全
CPU的頻率太快了,快到主存跟不上,這樣在處理器時鐘週期內,CPU經常須要等待主存,這樣就會浪費資源。因此cache的出現,是爲了環節CPU和內存之間速度的不匹配問題(結構:CPU -> cache -> memory)多線程
緩存的容量遠遠小於主存,所以出現緩存不命中的狀況在所不免,既然緩存不能包含CPU所須要的全部數據,那麼緩存的存在真的有意義嗎?架構
CPU cache是確定有它存在的意義的,至於CPU cache有什麼意義,那就要看一下它的局部性原理了:併發
1.時間局部性:若是某個數據被訪問,那麼在不久的未來它極可能再次被訪問
2.空間局部性:若是某個數據被訪問,那麼與它相鄰的數據很快也可能被訪問優化
多級緩存-緩存一致性(MESI),MESI是一個協議,這協議用於保證多個CPU cache之間緩存共享數據的一致性。它定義了CacheLine的四種數據狀態,而CPU對cache的四種操做可能會產生不一致的狀態。所以緩存控制器監聽到本地操做與遠程操做的時候須要對地址一致的CacheLine狀態作出必定的修改,從而保證數據在多個cache之間流轉的一致性。CacheLine的四種狀態以下:spa
- M: Modified 修改,指的是該緩存行只被緩存在該CPU的緩存中,而且是被修改過的,所以他與主存中的數據是不一致的, 該緩存行中的數據須要在將來的某個時間點(容許其餘CPU讀取主存相應中的內容以前)寫回主存,而當數據被寫回主存以後,該緩存行的狀態會變成E(獨享)
- E:Exclusive 獨享 緩存行只被緩存在該CPU的緩存中,是未被修改過的,與主存的數據是一致的,能夠在任什麼時候刻當有其餘CPU讀取該內存時,變成S(共享)狀態,一樣的當CPU修改該緩存行的內容時,會變成M(被修改)的狀態
- S:Share 共享,當前CPU和其餘CPU中都有共同數據,而且和主存中的數據一致;意味着該緩存行可能會被多個CPU進行緩存,而且各緩存中的數據與主存數據是一致的,當有一個CPU修改該緩存行時,在其餘CPU中的該緩存行是能夠被做廢的,變成I(無效的) 狀態
- I:Invalid 無效的,表明這個緩存是無效的,多是有其餘CPU修改了該緩存行;數據應該從主存中獲取,其餘CPU中可能有數據也可能無數據,當前CPU中的數據和主存被認爲是不一致的;對於invalid而言,在MESI協議中採起的是寫失效(write invalidate)。
MESI示意圖:
操作系統
CacheLine有四種數據狀態(MESI),而引發數據狀態轉換的CPU cache操做也有四種:線程
所以要完整的理解MESI這個協議,就須要把這16種狀態轉換的狀況理解清楚,狀態之間的相互轉換關係,可使用下圖進行表示:
在一個典型的多核系統中,每個核都會有本身的緩存來共享主存總線,每一個相應的CPU會發出讀寫(I/O)請求,而緩存的目的是爲了減小CPU讀寫共享主存的次數。一個緩存除了在 Invalid 狀態以外,均可以知足CPU的讀請求。
一個寫請求只有在該緩存行是M狀態,或者E狀態的時候纔可以被執行。若是當前狀態是處在S狀態的時候,它必須先將緩存中的該緩存行變成無效的(Invalid)狀態,這個操做一般做用於廣播的方式來完成。這個時候它既不容許不一樣的CPU同時修改同一個緩存行,即便修改該緩存行不一樣位置的數據也是不容許的,這裏主要解決的是緩存一致性的問題。一個處於M狀態的緩存行它必須時刻監聽全部試圖讀該緩存行相對就主存的操做,這種操做必須在緩存將該緩存行寫回主存並將狀態變成S狀態以前被延遲執行。
一個處於S狀態的緩存行也必須監聽其它緩存使該緩存行無效或者獨享該緩存行的請求,並將該緩存行變成無效(Invalid)。
一個處於E狀態的緩存行也必須監聽其它緩存讀主存中該緩存行的操做,一旦有這種操做,該緩存行須要變成S狀態。
所以,對於M和E兩種狀態而言老是精確的,他們在和該緩存行的真正狀態是一致的。而S狀態多是非一致的,若是一個緩存將處於S狀態的緩存行做廢了,而另外一個緩存實際上可能已經獨享了該緩存行,可是該緩存卻不會將該緩存行升遷爲E狀態,這是由於其它緩存不會廣播他們做廢掉該緩存行的通知,一樣因爲緩存並無保存該緩存行的copy的數量,所以(即便有這種通知)也沒有辦法肯定本身是否已經獨享了該緩存行。
從上面的意義看來E狀態是一種投機性的優化:若是一個CPU想修改一個處於S狀態的緩存行,總線事務須要將全部該緩存行的copy變成invalid狀態,而修改E狀態的緩存不須要使用總線事務。
什麼是亂序執行優化:
例如,我如今有兩個變量a和b,a的值爲10,b的值爲200,我要計算a乘以b的結果。而我在代碼上寫的是:
a=10;
b=200;
result=a*b;
可是到了CPU上的亂序執行優化後,可能就變成了:
b=200;
a=10;
result=a*b;
以下圖:
從上圖中,能夠看到CPU亂序執行優化後的代碼並不會對計算結果形成影響,但這也只是其中一種沒被影響的狀況而已。在單核時代,處理器保證作出的優化不會致使執行結果遠離預期目標。可是在多核環境下則並不是如此,由於在多核環境下同時會有多個核心在執行指令,每一個核心的指令均可能被亂序。另外處理器還引入了L一、L2等多級緩存機制,而每一個核心都有本身的緩存,這就致使了邏輯次序上後寫入的數據未必真的寫入了。若是咱們不作任何防禦措施,那麼處理器最終處理的結果可能與咱們代碼的邏輯結果大不相同。好比咱們在一個核心上執行數據寫入操做,並在最後寫一個標記用來表示以前的數據已經準備好了。而後從另一個核心上經過判斷這個標記來斷定所須要的數據是否已準備就緒,這種作法就存在必定的風險,標記位可能先被寫入,而數據並未準備完成,這個未完成既有多是沒有計算完成,也有多是緩存沒有被及時刷新到主存之中,這樣最終就會致使另外的核心使用了錯誤的數據,因此咱們才常常在多線程的狀況下保證線程安全。
以上咱們簡單介紹了在多核併發的環境下CPU進行亂序執行優化時所帶來的線程安全問題,爲了保證線程安全,咱們須要採起一些額外的手段去防止這種問題的發生。
不過在介紹如何採用實際手段解決這種問題以前,咱們先來看看Java虛擬機是如何解決這種問題的:爲了屏蔽各類硬件和操做系統內存的訪問差別,以實現讓Java程序在各類平臺下都能達到一致的併發效果,因此Java虛擬機規範中定義了Java內存模型(Java Memory Model簡稱JMM)。
Java內存模型是一種規範,它定義了Java虛擬機與計算機內存是如何協同工做的。它規定了一個線程如何和什麼時候能夠看到由其餘線程修改事後的共享變量的值,以及在必須時如何同步地訪問共享變量。
在明確了Java內存模型是作什麼的以後,咱們來看一下其中內存分配的兩個概念
Java內存模型要求調用棧和本地變量存放在線程棧(Thread Stack)上,而對象則存放在堆上。一個本地變量也多是指向一個對象的引用,這種狀況下這個保存對象引用的本地變量是存放在線程棧上的,可是對象自己則是存放在堆上的。
一個對象可能包含方法,而這些方法可能包含着本地變量,這些本地變量仍然是存放在線程棧上的。即便這些方法所屬的對象是存放在堆上的。一個對象的成員變量,可能會隨着所屬對象而存放在堆上,無論這個成員變量是原始類型仍是引用類型。靜態成員變量則是隨着類的定義一塊兒存放在堆上。
存放在堆上的對象,能夠被持有這個對象的引用的線程訪問。當一個線程能夠訪問某個對象時,它也能夠訪問該對象的成員變量。若是兩個線程同時調用同一個對象上的同一個方法,那麼它們都將會訪問這個方法中的成員變量,可是每個線程都擁有這個成員變量的私有拷貝。
硬件內存架構
現代硬件內存模型與Java內存模型有一些不一樣。理解內存模型架構以及Java內存模型如何與它協同工做也是很是重要的。這部分描述了通用的硬件內存架構,下面的部分將會描述Java內存是如何與它「聯手」工做的。
下圖簡單展現了現代計算機硬件內存架構:
CPU:一個現代計算機一般由兩個或者多個CPU,其中一些CPU還有多個核心。從這一點能夠看出,在一個有兩個或者多個CPU的現代計算機上同時運行多個線程是可能的。每一個CPU在某一時刻運行一個線程是沒有問題的。這意味着,若是你的Java程序是多線程的,在你的Java程序中每一個CPU上一個線程可能同時(併發)執行。
CPU Registers(寄存器):每一個CPU都包含一系列的寄存器,它們是CPU內內存的基礎。CPU在寄存器上執行操做的速度遠大於在主存上執行的速度。這是由於CPU訪問寄存器的速度遠大於主存。
CPU Cache(高速緩存):因爲計算機的存儲設備與處理器的處理設備有着幾個數量級的差距,因此現代計算機都會加入一層讀寫速度與處理器處理速度接近的高級緩存來做爲內存與處理器之間的緩存。這就是CPU中的緩存層,實際上絕大多數的現代CPU都有必定大小的緩存層。由於CPU訪問緩存層的速度快於訪問主存的速度,因此能夠將運算時使用到的數據複製到緩存中,讓運算可以快速的執行,當運算結束後,再從緩存同步到內存之中,這樣CPU就不須要等待緩慢的內存讀寫了。但一般訪問緩存比訪問內部寄存器的速度還要慢一點。而一些CPU還有多層緩存,但這些對理解Java內存模型如何和內存交互不是那麼重要。只要知道CPU中能夠有一個緩存層就能夠了。
運做原理:一般狀況下,當一個CPU須要讀取主存時,它會將主存的部分讀到CPU緩存中。它甚至可能將緩存中的部份內容讀到它的內部寄存器中,而後在寄存器中執行操做。當CPU須要將結果寫回到主存中去時,它會將內部寄存器的值刷新到緩存中,而後在某個時間點將值刷新回主存。
當CPU須要在緩存層存放一些東西的時候,存放在緩存中的內容一般會被刷新回主存。CPU緩存能夠在某一時刻將數據局部寫到它的內存中,和在某一時刻局部刷新它的內存。它不會再某一時刻讀/寫整個緩存。一般,在一個被稱做「cache lines」的更小的內存塊中緩存被更新。一個或者多個緩存行可能被讀到緩存,一個或者多個緩存行可能再被刷新回主存。
Java內存模型和硬件內存架構之間的橋接
上面已經提到,Java內存模型與硬件內存架構之間存在差別。硬件內存架構沒有區分線程棧和堆。對於硬件而言,全部的線程棧和堆都分佈在主內存中。部分線程棧和堆可能有時候會出如今CPU緩存中和CPU內部的寄存器中。以下圖所示:
線程和主內存的抽象關係
Java內存模型抽象結構圖:
每一個線程之間的共享變量存儲在主內存裏面,每一個線程都有一個私有的本地內存,本地內存是Java內存模型的一個抽象的概念,並非真實存在的。它涵蓋了緩存、寫緩存區、寄存器以及其餘的硬件和編譯器的優化,本地內存中存儲了該線程已讀或寫共享變量的拷貝的一個副本。
從一個更低的層次來講,主內存就是硬件的內存,而爲了獲取更好的運行速度,虛擬機及硬件系統可能會讓工做內存優先存儲於寄存器和高速緩存中。
Java內存模型中的線程的工做內存(working memory)是cpu的寄存器和高速緩存的抽象描述。而JVM的靜態內存存儲模型(JVM內存模型)只是一種對內存的物理劃分而已,它只侷限在內存,並且只侷限在JVM的內存。
若是上圖中的線程A和線程B要通訊,必須經歷兩個步驟:
所以,多線程的環境下就會出現線程安全問題。例如咱們要進行一個計數的操做:線程A在主內存中讀取到了變量值爲1,而後保存到本地內存A中進行累加。就在此時線程B並無等待線程A把累加後的結果寫入到主內存中再進行讀取,而是在主內存中直接讀取到了變量值爲1,而後保存到本地內存B中進行累加。此時,兩個線程之間的數據是不可見的,當兩個線程同時把計算後的結果都寫入到主內存中,就致使了計算結果是錯誤的。這種狀況下,咱們就須要採起一些同步的手段,確保在併發環境下,程序處理結果的準確性。
採起同步手段時的八種操做
同步規則
同步操做與規則: