1. 程式人生 > >(2.1.27.1)Java併發程式設計:併發

(2.1.27.1)Java併發程式設計:併發

  1. 在物理計算機中CPU為了提高處理速度,添加了快取記憶體與CPU亂序執行

一、 併發的起源

為了提高計算機處理資料的速度,現代的計算機都支援多工處理。

在32位windows作業系統中 ,多工處理是指系統可同時執行多個程序,而每個程序也可同時執行多個執行緒。一個執行緒是指程式的一條執行路徑,它在系統指定的時間片中完成特定的功能。系統不停地在多個執行緒之間切換,由於時間很短,看上去多個執行緒在同時執行。或者對於線上程式可並行執行同時服務於多個使用者稱為多工處理。

在本篇中我們就主要講解:使用 [快取記憶體][亂序執行] 來提高CPU(處理器)的資料處理速度,所引發的問題。

二、物理計算機的記憶體模型

理解java記憶體模型之前,我們先來了解一下,物理計算機的記憶體模型,其對Java記憶體模型有著很大的參考意義。

在物理計算機中:

  1. 我們需要處理的資料都在記憶體中
  2. 處理器處理資料,需要從記憶體中獲取相應的資料,然後存入記憶體中。

為了提高計算機的處理速度(讀取資料,儲存資料有IO消耗),我們常常會在CPU(處理器)中加入快取記憶體(Cache Memory)

快取記憶體(Cache Memory)是位於CPU與記憶體之間的臨時儲存器,它的容量比記憶體小的多但是交換速度卻比記憶體要快得多。 快取記憶體的出現主要是為了解決CPU運算速度與記憶體讀寫速度不匹配的矛盾,因為CPU運算速度要比記憶體讀寫速度快很多,這樣會使CPU花費很長時間等待資料到來或把資料寫入記憶體。在快取中的資料是記憶體中的一小部分,但這一小部分是短時間內CPU即將訪問的,當CPU呼叫大量資料時,就可先快取中呼叫,從而加快讀取速度。

快取記憶體也就是指將資料快取到處理器中,當處理器處理完資料後,再將處理的資料結果儲存在記憶體中。具體如下圖所示:

在這裡插入圖片描述

當CPU(處理器)要讀取一個數據時:

  1. 首先從一級快取中查詢
  2. 如果沒有找到再從二級快取中查詢
  3. 如果還是沒有就從三級快取或記憶體中查詢。

一般來說,每級快取的命中率大概都在80%左右,也就是說全部資料量的80%都可以在一級快取中找到,只剩下20%的總資料量才需要從二級快取、三級快取或記憶體中讀取。

三、快取結構在多執行緒模型中引發的快取不一致問題

雖然高速緩緩衝提高了CPU(處理器)處理資料的速度問題,但是其在多執行緒中執行就會有問題了。

在多核CPU中,每條執行緒可能運行於不同的CPU中,因此每個執行緒執行時有自己的快取記憶體(對單核CPU來說,其實也會出現這種問題,只不過是以執行緒排程的形式來分別執行的)。這時CPU快取中的值可能和快取中的值不一樣

,這就會出現快取不一致的問題。

為了解決該問題。物理機算計提供了兩種方案來解決該問題。具體如下圖所示:

在這裡插入圖片描述 【圖快取結構在多執行緒模型中引發的快取不一致問題】

3.1 通過匯流排加LOCK#鎖的方式

在早期的CPU當中,是通過在總線上加LOCK#鎖的形式來解決快取不一致的問題。匯流排(Bus)是計算機各種功能部件之間傳送資訊的公共通訊幹線,它是由導線組成的傳輸線束,在計算機中資料是通過匯流排,在處理器和記憶體之間傳遞。

在這裡插入圖片描述 【快取不一致:通過匯流排加LOCK#鎖的方式】

因為CPU和其他部件進行通訊都是通過匯流排來進行的,如果對匯流排加LOCK#鎖的話,也就是說阻塞了其他CPU對其他部件訪問(如記憶體),從而使得只能有一個CPU能使用這個變數的記憶體。 在總線上發出了LCOK#鎖的訊號,那麼只有等待這段程式碼完全執行完畢之後,其他CPU才能從其記憶體讀取變數,然後進行相應的操作。 這樣就解決了快取不一致的問題。

3.2 通過快取一致性協議的方式

對於[匯流排加LOCK#鎖]的方式,由於在鎖住匯流排期間,其他CPU無法訪問記憶體,會導致效率低下。因此出現了第二種解決方案:通過快取一致性協議來解決快取一致性問題。

最出名的就是Intel 的MESI協議,MESI協議保證了每個快取中使用的共享變數的副本是一致的。

它核心的思想是:

  1. 當CPU寫資料時,如果發現操作的變數是共享變數(即在其他CPU中也存在該變數的副本),會發出訊號通知其他CPU將該變數的快取行置為無效狀態
  2. 因此當其他CPU需要讀取這個變數時,發現自己快取中快取該變數的快取行是無效的,那麼它就會從記憶體重新讀取。

四、CPU(處理器)的亂序執行(out-of-orderexecution)

除了使用快取記憶體來提高CPU(處理器)的資料處理速度,CPU(處理器)還採用了允許將多條指令不按程式規定的順序分開發送給各相應電路單元處理的技術。 在這期間:

  1. 不按規定順序的執行指令
  2. 然後由重新排列單元將各執行單元結果按指令順序重新排列。

採用亂序執行技術的目的是為了使CPU內部電路滿負荷運轉並相應提高了CPU的執行程式的速度。下面這個例子幫助大家理解:

在這裡插入圖片描述 【亂序執行:順序執行示例】

假如請A、B、C三個名人為晚會題寫橫幅“春節聯歡晚會”六個大字,每人各寫兩個字。如果這時在一張大紙上按順序由A寫好"春節"後再交給B寫"聯歡",然後再由C寫"晚會",那麼這樣在A寫的時候,B和C必須等待,而在B寫的時候C仍然要等待而A已經沒事了。

在這裡插入圖片描述 【亂序執行:亂序執行示例】

但如果採用三個人分別用三張紙同時寫的做法, 那麼B和C都不必須等待就可以同時各寫各的了,甚至C和B還可以比A先寫好也沒關係(就象亂序執行)。當他們都寫完後就必須重新在橫幅上(自然可以由別人做,就象CPU中亂序執行後的重新排列單元)按"春節聯歡晚會"的順序排好才能掛出去。

亂序執行是指單個執行緒中的順序程式碼可能會亂序執行

雖然引入了亂序執行來提高cpu的效率,但是CPU不會對任務操作進行重排序,編譯器與處理器只會對沒有資料依賴性的指令進行重排序。

這裡提到了一個關鍵詞資料依賴性。什麼是資料依賴呢?

4.1 資料依賴

如果兩個操作訪問同一個變數,且這兩個操作中有一個為寫操作,此時這兩個操作之間就存在資料依賴性。

名稱 程式碼示例 說明
寫後讀 a=1;b=a 寫一個變數之後,再讀這個位置
寫後寫 a=1;a=2 寫一個變數之後,再寫這個位置
讀後寫 a=b;b=1 讀一個變數之後,再寫這個位置

上述三種情況,a與b存在著“資料依賴性”

同時大家也要注意:這裡所說的資料依賴性是指**單個處理器執行的指令序列和單個執行緒中執行的操作,多處理器和不同執行緒之間是沒有資料依賴性這種關係的**。

4.2 重排序規則(as-if-serial)

既然我們已經知道了CPU在處理資料時候會出現重排序。那重排序的規則是什麼呢?

重排序規則:不管怎麼重排序(編譯器和處理器為了提高並行度),單執行緒(程式)執行結果不能被改變。(當然,多執行緒中還是無法保證,後文會講到)

編譯器、runtime和處理器都必須遵守。那麼我們三角形面積示例程式碼說明:

double a = 3;//底
double h = 10;//高
double s = a*h/2//面積

a與s存在資料依賴關係,同時h與s也存在依賴關係。 因此在程式的最終指令執行時: s是不能排在a與h之前。 因為a與h不存在著資料依賴關係。所以處理器可以對a與h之前的執行順序重排序。

在這裡插入圖片描述 【重排序規則:三角面積】

經過處理器的重排序後,執行的結果並沒有發生改變。

在這裡插入圖片描述 【重排序規則:三角面積2】