java併發基礎(二)

一、CPU多級緩存:

數據的讀取和存儲都經過高速緩存,CPU核心和高速緩存之間有一條快速通道,在上方簡化的圖中,主存和高速緩存都連接在系統總線上。

緩存容量遠遠小於主存,

一般二級緩存大於一級緩存容量,但速度比一級慢,三級緩存大於二級緩存,但是速度更慢。

1、爲什麼需要CPU cache ?

  答:CPU的頻率太快了,快到主存跟不上,這樣在處理器時鐘週期內,CPU常常需要等待主存,浪費資源。所以cache的出現,是爲了緩解CPU和內存之間速度的不匹配問題(結構:cpu——》cache——》memory)

2、CPU cache 有什麼意義:

1)時間局部性:如果某個數據被訪問,那麼在不久的將來它很可能被再次訪問;

2)空間局部性:如果某個數據被訪問,那麼與它相鄰的數據很快也可能被訪問;

二、CPU多級緩存——緩存一致性問題(MESI):

》用於保證多個CPU cache之間緩存共享數據的一致:

MESI是一個協議,爲了保證多個CPU緩存共享數據的一致性的問題,定義了cache line的四種狀態,

而CPU對cache的四種操作可能會產生不一致的狀態,因此緩存控制器監聽到本地 操作和遠程操作的時候,

需要對地址異徑的cache line的狀態做出一定的修改,從而保證數據在多個緩存之間流轉的一致性。

》》其中MESI 是四個單詞的縮寫,M 爲 modified  表示修改     E: exclusive 表示獨享   S :Shared  表示共享   I :invalid  表示無效

M :表示被修改,指該緩存行,值被緩存在該CPU的緩存中,並且是被修改過的。因此它與主存中的數據是不一樣的,該緩存行中的內存需要在未來的某個時間點寫回主存。這個時間點我們是允許其他CPU讀取主存中相應的內存之前,當這裏的值被寫回主存之後呢,該緩存行的狀態會變成E 的狀態,就是獨享。

E:獨享狀態的緩存行,值被緩存在該CPU的緩存中,它是未被修改過的,是和主存中的數據是一致的。這個狀態,可以在任何時刻,當有其他CPU讀取該內存時變成S狀態,即共享狀態。同樣的,當CPU修改該緩存行的內容時,該狀態可以變成M 狀態,即modified 的狀態。

S :共享狀態,意味着該緩存行可能被多個CPU進行緩存,並且各個緩存中的數據與主存數據是一致的。當有一個CPU修改該緩存行時,其他CPU從該緩存行是可以被作廢的,變成invalid的狀態。

I :無效狀態,就是該緩存行的數據是無效的,可能是有其他CPU修改了該緩存行。

》》四種操作包括:

local read: 讀本地緩存中的數據

local write:將數據寫入到本地緩存中

remote read:將內存中的數據讀取過來

remote write :將數據寫回到主存中去。

要完整理解MESI協議,將四種操作和四種狀態組成的16種狀態轉換考慮清楚。

在多核系統中,每個核都有自己的緩存來共享主存總線,每個相應的CPU會發出讀寫請求,而緩存的目的是爲了減少CPU讀寫共享主存的次數。一個緩存除了在invalid的狀態之外,都可以滿足CPU的讀請求。比如下圖,除了在invalid的狀態下,都是可以執行local read、 remote read操作。而一個寫請求,只有在該緩存行是M 狀態或者E 狀態才能被執行。如果該緩存行當前狀態是S狀態時,必須先將緩存中的該緩存行的狀態變爲無效狀態,這個操作通常用廣播的方式來完成。這時既不允許不同的CPU同時修改同一個緩存行,即使修改同一個緩存行不同位置的不同數據也是不允許的。這裏主要是解決緩存不一致的問題。

 一個處於M 狀態的緩存行,必須時刻監聽所有試圖讀該緩存行相對讀主存的操作,這種操作必須在緩存將該緩存行寫回到主存,並將狀態變成S狀態之前被延遲執行。

一個處於S 狀態的緩存行,必須監聽其他緩存使該緩存行無效或者獨享該緩存行的請求,並將該緩存行置爲無效。

一個處於E狀態的緩存行,它要監聽其他緩存讀緩存中該緩存行的操作,一旦有該緩存行的操作,就要變爲S共享狀態。因此對於M 和E 兩種狀態而言,數據總是jingqu的。他們在和緩存行的狀態總是一致的。而S狀態可能是不一致的。如果一個緩存將處於S狀態的緩存行作廢,另一個緩存實際上可能已經獨享了該緩存行,但該緩存卻不會將該緩存行升遷爲E狀態,這是因爲其他緩存不會廣播他們作廢掉該緩存行的通知。同樣,因爲該緩存沒有保存該緩存行copy的數量,因此也沒有辦法確定該緩存行是否被自己獨享了。從上面的意義來看,E狀態更像是一種投機性的優化。因爲CPU想修改一個處於S 共享狀態的緩存行,總線事務需要將所有copy的值變爲invalid的狀態纔可以。而修改E狀態的緩存,卻不需要使用總線事務。

二、CPU多級緩存——亂序執行優化

》處理器爲提高運算速度而做出違背代碼原有順序的優化;

如上,爲了計算a*b這個式子,計算機在賦值時,可能不是按照初始的先 a=10,b=200,而是有可能是先b=200,後a=10類似這種。單核時代,這種先後不會使得結果出現異常,但是多核時代有可能出現異常。比如後賦值的變量可能不是最後被賦值。

比如多核中,在一個核上執行數據寫入操作,並在最後寫一個標記,表示之前的數據已經準備好了。然後在另一個核上判斷該標記標記的數據是否已經準備好了,這就存在一定的風險,比如,標記的數據先被寫入,但實際上數據還未完成,這個完成,可能是計算未完成,也可能是數據沒有從緩存刷新到主存當中。最終導致另一個核讀取到了錯誤的數據。

三、java內存模型(Java Memory Model, JMM):

爲了保證在不同平臺下實現相同併發的效果,java規範中定義了內存模型。

JMM是一種規範,它規定了java虛擬機與計算機內存之間是如何協同工作的,它規定了一個線程如何和何時可以看到其他線程修改過後的共享變量的值,以及在必需時,如何同步地訪問共享變量。

 堆:是運行時的一個數據區,它的數據由java垃圾回收器回收,

        優勢:可以在運行時動態地分配內存大小,生存期也不必事先告訴編譯器。因爲它是在運行時動態地分配內存的。java垃圾回收器會自動回收不再需要的數據。

         劣勢:由於是要在運行時動態分配內存,因此存取速度比較慢。

棧:和堆相比,

      優勢:存取速度快,僅次於計算機中的寄存器。它可以共享

      缺點:存放的數據必須是類型、大小和生存期必須是確定的,否則編譯會報錯,缺乏一定的靈活性。棧中主要存放java一些基本類型變量,比如小寫的int double String 等。JAVA 內存模型JMM要求調用棧和本地變量存放在線程棧上,對象存放在堆上。一個本地變量可能指向一個對象的引用,此時引用放在線程棧上,而對象本身實際存放在堆上。一個對象的成員變量隨着對象本身存放在堆上,靜態成員變量隨着類的定義存放在堆上。存放在堆上的對象可以被持有該對象引用的線程訪問。當一個線程可以訪問一個對象時,也可以訪問該對象的成員變量。如果兩個線程同時調用同一個對象的同一個方法,它們都會訪問這個對象的成員變量,但是每一個線程都擁有了這個成員變量的私有拷貝,這點非常重要!!

1、計算機硬件架構圖:

 多CPU,一個CPU可能還有多個核。如果你的java程序運行在多CPU計算機上,同時運行多個線程是完全有可能的。每個CPU運行一個線程是沒問題,一個java程序運行在多CPU的環境中,併發是非常有可能的。

CPU的寄存器是CPU內存的基礎,每個CPU包含一系列的寄存器。CPU在寄存器上執行的速度遠大於主存,因爲CPU訪問寄存器的速度遠大於主存.

高速緩存,CPU的頻率遠大於主存,爲了提高和保證運行速度,建立了運行速度接近於CPU的高速緩存,作爲CPU和主存之間的緩衝。當計算時,把需要的數據放到緩存中,讓運算能快速進行,運算結束後,再把數據從高速緩存同步到主存當中,這樣就不用讓CPU等待內存緩慢的讀寫了。在某個時刻,可能有一個或者可能會有多個緩存行被讀到緩存,也可能有一個或者多個緩存行被刷回到主存中。也就是同一個時刻,可能有多個操作。

所有的CPU都可以訪問主存,通常比CPU的高速緩存大地多。

2、java內存模型和硬件架構模型之間的關聯圖分析:

硬件模型沒有區分棧和堆等,對於硬件而言,所有的java內存模型中提到的線程堆和棧都分佈在主存中,雖然有時候,棧和堆會出現在CPU的高速緩存或者CPU寄存器中。

 

 共享變量是存放在CPU的主存中的,而上圖中本地內存A、B是抽象的,不是真實存在的,是java線程中的邏輯概念。每個線程都有一個私有的本地內存,它涵蓋了緩存、寫緩存區、寄存器以及其他硬件和編譯器的優化。本地線程存儲了一個共享變量的副本。即如果線程要訪問一個共享變量,它就拷貝該共享變量一個副本放在本地內存中。從更低的層次來說,主內存就是硬件的內存。而爲了獲取更好的運行速度,虛擬機及硬件系統可能會讓工作內存優先存放在CPU的寄存器和高速緩存中,JMM中的線程的工作內存是CPU中的寄存器和高速緩存的一個抽象的描述,而JVM java 靜態內存存儲模型,只是一種對內存的物理劃分而已,它只限於內存,而且只侷限在jvm的內存。現在如果線程間通信,必須要經歷主內存,即線程A 從內存中讀取數據到本地高速緩存,再到寄存器,計算完畢,再將數據從CPU的寄存器刷新到高速緩存,再刷新到主存中。而線程B 也可能這樣的操作。

3、所以分析之前的,多線程,對一個共享變量執行累加操作時會發生異常,原因就是,

線程A 在進行如上讀取內存數據到本地,計算執行完畢再刷新到主存中時,B線程也在這麼操作,而兩個線程之間因爲操作不可見,造成數據沒有及時同步,導致結果不符合預期。比如A 執行完,寫回到內存後,B線程即將也要寫數據到內存中,但寫數據到內存前,未確認(在B 讀取 到本地、計算、 刷新到主存這個過程中)是否有其他線程有修改過該數據。

4、對此要提出一些概念規則。java內存同步方法和規則。

》》》java內存模型——同步的八種操作:

》lock(鎖定):作用於主內存的變量,把一個變量標識爲一條線程獨佔狀態;

》unlock(解鎖):作用於主內存的變量,把一個處於鎖定狀態的變量釋放出來,釋放後的變量纔可以被其他線程鎖定;

》read(讀取):作用於主內存的變量,把一個變量值從主內存傳輸到線程的工作內存中,以便隨後的load動作使用;

》load(載入):作用於工作內存的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中;

》use(使用):作用於工作內存的變量,把工作內存中的一個變量值傳遞給執行引擎;

》assign(賦值):作用於工作內存的變量,它把一個從執行引擎接收到的值賦值給工作內存的變量;

》store(存儲):作用於工作內存的變量,把工作內存中的一個變量的值傳送到主內存中,以便隨後的write的操作;

》write(寫入):作用於主內存的變量,它把store操作從工作內存中一個變量的值傳送到主內存的變量中。

》》》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操作);

5、併發的優勢與風險: 

通常網絡io或者磁盤io 比CPU和內存的io緩慢很多。

總結:

》CPU多級緩存:緩存一致性、亂序執行優化;

》java內存模型:JMM規定、抽象結構、同步八種操作及規則;

》併發的優勢與風險;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章