1. 程式人生 > 實用技巧 >Java多執行緒程式設計,CPU快取和記憶體屏障

Java多執行緒程式設計,CPU快取和記憶體屏障

一、CPU三級快取

1、快取的作用

  CPU的結構很複雜,簡單地說由運算器和暫存器組成。程式執行時,需要CPU去執行運算,運算是由運算器來執行,運算器可以做加減乘除運算以及與或非邏輯運算,運算過程中可能需要臨時存放資料到某個地方,暫存器就起到這個作用。

  雖然暫存器可以儲存一些執行時資料,但是容量是很小的,程式執行時產生的大部分資料(比如Java物件)是儲存在記憶體中的,並且程式指令也是儲存在記憶體中,所以程式執行時CPU需要頻繁操作記憶體,包括讀取和寫入,但是CPU的速度太快了,如果直接操作記憶體,CPU的大部分時間會處於等待記憶體操作的空轉狀態,記憶體完全跟不上節奏,怎麼辦?

  這時候就需要有快取的存在了,記憶體將CPU要讀取的資料來源源不斷地載入到快取中,CPU讀取快取,快取的速度比記憶體快多了,勉強能跟得上CPU大哥的節奏了!

  但是CPU表示快取你還是太慢了,我帶不動,所以產生了一級快取、二級快取、三級快取,一級快取最快、二級次之、三級最慢;快取容量則反過來,一級最小,二級大一些,三級最大。
  為什麼快取能加快系統執行?舉個例子,現在需要很多水,如果直接開啟水龍頭,要放很久,如果有水桶已經放滿了水,取水是不是會快點?如果需要更多的水,我們弄個水塔,平時儲滿水,假如水桶的水不夠用,則開啟水塔,這樣就達到快速取水的目的。
  快取可以看成是一個數據的池子,由於速度越快的快取單位儲存空間的價格也越高,所以要有多級快取,速度快的儲存小,速度慢的儲存大,多級快取結合達到總體上經濟又實惠的效果,在三級快取中,每一級快取都有80%左右的命中率,如果本級快取中找不到CPU要的資料,則進入下一級快取中查詢,三級快取中找不到則進入記憶體查詢,這種可能性只有0.8%,大多數情況下可以保證了CPU快速執行,避免記憶體延遲。

  • L1 Cahce(一級快取)是CPU第一層快取記憶體,分為資料快取和指令快取,一般伺服器CPU的L1快取的容量通常在32-4096KB。
  • L2 是由於L1快取記憶體容量的限制,為了再次提高CPU的運算速度,在CPU外部放置一高速儲存器,即二級快取。
  • L3 快取的應用可以進一步降低記憶體延遲,同時提升大資料量計算時處理器的效能;具有較大L3快取的處理器可以提供更有效的檔案系統快取行為及較簡訊息和處理器佇列長度;現在的計算機都內建了L3,並且多核計算機中多個CPU可以共享一個L3快取,但是每個CPU都會有它自己的L1、L2。

  CPU在讀取資料時,先在L1中尋找,再從L2尋找,再從L3尋找,然後是記憶體,最後是外儲存器。

2、快取同步協議

  對於多核計算機,多個CPU可能會讀取同樣的資料進行快取,在經過不同運算之後,最終寫入主記憶體,那麼問題來了,寫入的時候誰先誰後,最終寫入主記憶體中的資料以哪個CPU為準?
  為了應對這種快取記憶體回寫的場景,眾多CPU廠商聯合制定了快取一致性協議MESI協議,並分別實現,MESI協議規定每條快取有個狀態位,同時定義了下面四個狀態:

  • 修改態(Modified)- 此cache行已被修改過(髒行),內容已不同於主存,為此cache專有;
  • 專有態(Exclusive)- 此cache行內容同於主存,但不出現於其他cache中;
  • 共享態(Shared)- 此cache行內容同於主存,但也出現於其他cache中;
  • 無效態(Invalid)- 此cache行內容無效(空行)。

  當計算機中有多個處理器時,單個CPU對快取中資料進行了改動,需要通知給其他CPU;這意味著CPU不僅要控制自己的讀寫操作,還要監聽其他CPU發出的通知,從而保證最終一致。

3、快取記憶體存在問題

  快取中的資料與主記憶體的資料並不是實時同步的,各CPU(或CPU核心)間快取的資料也不是實時同步的;在同一時間點,各CPU所看到同一記憶體地址的資料的值可能是不一致的。


二、效能優化 - 執行時指令重排

  執行時指令重排是CPU為了避免阻塞等待某些操作需要的資源,先去執行可執行的指令,當阻塞等待的資源獲取到時,再去執行對應的指令的操作

1、程式碼示例


  指令重排的場景:當CPU寫快取時發現快取區塊正在被其他CPU佔用,為了提高CPU處理效能,可能將後面的讀快取命令優先執行。
【注意】對於單執行緒程式來說,需要遵循as-if-serial語義,即不管指令如何被CPU重排,最終執行的效果都是一致的。

2、as-if-serial語義

  不管怎麼重排序(編譯器和處理器為了提高並行度),單執行緒程式的執行結果不能被改變,編譯器、runtime和處理器都必須遵循as-if-serial語義,也就是說,編譯器和處理器不會對存在資料依賴關係的操作做重排序。

3、指令重排存在問題

  但是對多執行緒程式來說,指令邏輯無法分辨因果關聯,因此指令重排可能會出現亂序執行,導致程式執行結果錯誤,因此在多執行緒程式中有些時候需要通類似volatile修飾變數之類的方式來避免指令重排。


三、記憶體屏障

  為了解決快取記憶體導致的快取記憶體資料一致性問題,以及指令重排導致的程式亂序出錯問題,處理器提供了兩個記憶體屏障指令(Memory Barrier)。

1、寫記憶體屏障(Store Memory Barrier)

  在指令後插入Store Barrier,能讓寫入快取中的最新資料更新寫入主記憶體,讓其他執行緒可見;當發生這種強制寫入主記憶體的顯式呼叫,CPU就不會處於效能優化考慮進行指令重排。

2、讀記憶體屏障(Load Memory Barrier)

  在指令前插入Load Barrier,可以讓快取記憶體中的資料失效,強制重新從主記憶體載入資料,讓CPU快取與主記憶體保持一致,避免快取導致的一致性問題。