併發編程(二)緩存一致性與JMM

#CPU高速緩存
線程是CPU調度的最小單元,線程涉及的目的最終仍然是更充分的利用計算機處理的效能,但是絕大部分的運算任務不能只依靠處理器“計算”就能完成,處
理器還需要與內存交互,比如讀取運算數據、存儲運算結果,這個I/O操作是很難消除的。
而由於計算機的存儲設備與處理器的運算速度差距非常大,所以現代計算機系統都會增加一層讀寫速度儘可能接近處理器運算速度的高速緩存來作爲內存和處理器之間的緩衝:
將運算需要使用的數據複製到緩存中,讓運算能快速進行,當運算結束後再從緩存同步到內存之中。
這裏寫圖片描述
高速緩存從下到上越接近CPU速度越快,同時容量也越小。現在大部分的處理器都有二級或者三級緩存,從下到上依次爲 L3 cache, L2 cache, L1 cache. 緩存又可以分爲指令緩存和數據緩存,指令緩存用來緩存程序的代碼,數據緩存用來緩存程序的數據
L1 Cache,一級緩存,本地core的緩存,分成32K的數據緩存L1d和32k指令緩存L1i,訪問L1需要3cycles,耗時大約1ns;
L2 Cache,二級緩存,本地core的緩存,被設計爲L1緩存與共享的L3緩存之間的緩衝,大小爲256K,訪問L2需要12cycles,耗時大約3ns;
L3 Cache,三級緩存,在同插槽的所有core共享L3緩存,分爲多個2M的段,訪問L3需要38cycles,耗時大約12ns;
#緩存一致性問題
CPU-0讀取主存的數據,緩存到CPU-0的高速緩存中,CPU-1也做了同樣的事情,而CPU-1把count的值修改成了2,
並且同步到CPU-1的高速緩存,但是這個修改以後的值並沒有寫入到主存中,CPU-0訪問該字節,由於緩存沒有更新,所以仍然是之前的值,就會導致數據不一致的問題
引發這個問題的原因是因爲多核心CPU情況下存在指令並行執行,而各個CPU核心之間的數據不共享從而導致緩存一致性問題,爲了解決這個問題,CPU生產廠商提供了相應的解決方案
總線鎖
當一個CPU對其緩存中的數據進行操作的時候,往總線中發送一個Lock信號。其他處理器的請求將會被阻塞,那麼該處理器可以獨佔共享內存。
總線鎖相當於把CPU和內存之間的通信鎖住了,所以這種方式會導致CPU的性能下降,所以P6系列以後的處理器,出現了另外一種方式,就是緩存鎖。
緩存鎖
如果緩存在處理器緩存行中的內存區域在LOCK操作期間被鎖定,當它執行鎖操作回寫內存時,處理不在總線上聲明LOCK信號,而是修改內部的緩存地址,
然後通過緩存一致性機制來保證操作的原子性,因爲緩存一致性機制會阻止同時修改被兩個以上處理器緩存的內存區域的數據,當其他處理器回寫已經被鎖定的緩存行的數據時會導致該緩存行無效。
所以如果聲明瞭CPU的鎖機制,會生成一個LOCK指令,會產生兩個作用

  1. Lock前綴指令會引起處理器緩存回寫到內存,在P6以後的處理器中,LOCK信號一般不鎖總線,而是鎖緩存
  2. 一個處理器的緩存回寫到內存會導致其他處理器的緩存無效
    #緩存一致性協議
    處理器上有一套完整的協議,來保證Cache的一致性,比較經典的應該就是MESI協議了,它的方法是在CPU緩存中保存一個標記位,這個標記爲有四種狀態
    Ø M(Modified) 修改緩存,當前CPU緩存已經被修改,表示已經和內存中的數據不一致了
    Ø I(Invalid) 失效緩存,說明CPU的緩存已經不能使用了
    Ø E(Exclusive) 獨佔緩存,當前cpu的緩存和內存中數據保持一直,而且其他處理器沒有緩存該數據
    Ø S(Shared) 共享緩存,數據和內存中數據一致,並且該數據存在多個cpu緩存中
    每個Core的Cache控制器不僅知道自己的讀寫操作,也監聽其它Cache的讀寫操作,嗅探(snooping)"協議
    單核Cache中每個Cache line有2個標誌:dirty和valid標誌,它們很好的描述了Cache和Memory(內存)之間的數據關係(數據是否有效,數據是否被修改),而在多核處理器中,多個核會共享一些數據,MESI協議就包含了描述共享的狀態。
    在MESI協議中,每個Cache line有4個狀態,可用2個bit表示,它們分別是:
    M(Modified)這行數據有效,數據被修改了,和內存中的數據不一致,數據只存在於本Cache中。
    E(Exclusive)這行數據有效,數據和內存中的數據一致,數據只存在於本Cache中。
    S(Shared)這行數據有效,數據和內存中的數據一致,數據存在於很多Cache中。
    I(Invalid)這行數據無效。
    M(Modified)和E(Exclusive)狀態的Cache line,數據是獨有的,不同點在於M狀態的數據是dirty的(和內存的不一致),E狀態的數據是clean的(和內存的一致)。
    S(Shared)狀態的Cache line,數據和其他Core的Cache共享。只有clean的數據才能被多個Cache共享。
    I(Invalid)表示這個Cache line無效
    MESI狀態之間的遷移過程如下:
    這裏寫圖片描述
    這裏寫圖片描述
    Intel的core i7處理器使用從MESI中演化出的MESIF協議,F(Forward)從Share中演化而來,一個Cache line如果是Forward狀態,它可以把數據直接傳給其它內核的Cache,而Share則不能
    #CPU 的優化執行
    除了增加高速緩存以爲,爲了更充分利用處理器內內部的運算單元,處理器可
    能會對輸入的代碼進行亂序執行優化,處理器會在計算之後將亂序執行的結果
    充足,保證該結果與順序執行的結果一直,但並不保證程序中各個語句計算的
    先後順序與輸入代碼中的順序一致,這個是
    處理器的優化執行
    ;還有一個就是
    編程語言的編譯器也會有類似的優化,比如做指令重排來提升性能。
    #併發編程的問題
    其實原子性、可見性、有序性問題,是我們抽象出來的概念,他們的核心
    本質就是剛剛提到的緩存一致性問題、處理器優化問題導致的指令重排序問
    題。比如
    緩存一致性就導致可見性問題
    處理器的亂序執行會導致原子性問題指令重排會導致有序性問題。爲了解決這些問題,所以在 JVM 中引入了 JMM 的概念
    #java內存模型
    內存模型定義了共享內存系統中多線程程序讀寫操作行爲的
    規範
    ,來屏蔽各種
    硬件和操作系統的內存訪問差異,來實現 Java 程序在各個平臺下都能達到一致
    的內存訪問效果。Java 內存模型的主要目標是定義程序中各個變量的
    訪問規則
    ,也就是在虛擬機中將變量存儲到內存以及從內存中取出變量(這裏的變
    量,指的是共享變量,也就是實例對象、靜態字段、數組對象等存儲在堆內存
    中的變量。而對於局部變量這類的,屬於線程私有,不會被共享)這類的底層
    細節。通過這些規則來規範對內存的讀寫操作,從而保證指令執行的正確性。
    它與處理器有關、與緩存有關、與併發有關、與編譯器也有關。他解決了 CPU
    多級緩存、處理器優化、指令重排等導致的內存訪問問題,保證了併發場景下
    的可見性、原子性和有序性,。內存模型解決併發問題主要採用兩種方式:限制處理器優化和使用內存屏障
    Java 內存模型定義了線程和內存的交互方式,在 JMM 抽象模型中,分爲主內存、工作內存。主內存是所有線程共享的,工作內存是每個線程獨有的。線程對變量的所有操作(讀取、賦值)都必須在工作內存中進行,不能直接讀寫主內存中的變量。並且不同的線程之間無法訪問對方工作內存中的變量,線程間的變量值的傳遞都需要通過主內存來完成,他們三者的交互關係如下:
    這裏寫圖片描述
    總的來說,JMM 是一種規範,目的是解決由於多線程通過共享內存進行通信時,存在的本地內存數據不一致、編譯器會對代碼指令重排序、處理器會對代碼亂序執行等帶來的問題。目的是保證併發編程場景中的原子性、可見性和有序性。
    #原子性
    即一個操作或者多個操作 要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行
    在java中提供了兩個高級的字節碼指令monitorenter和monitorexit,在Java中對應的Synchronized來保證代碼塊內的操作是原子的
    #可見性
    指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
    Java中的volatile關鍵字提供了一個功能,那就是被其修飾的變量在被修改後可以立即同步到主內存,被其修飾的變量在每次是用之前都從主內存刷新。因此,可以使用volatile來保證多線程操作時變量的可見性。除了volatile,Java中的synchronized和final兩個關鍵字也可以實現可見性
    #有序性
    即程序執行的順序按照代碼的先後順序執行
    在Java中,可以使用synchronized和volatile來保證多線程之間操作的有序性。實現方式有所區別:volatile關鍵字會禁止指令重排。synchronized關鍵字保證同一時刻只允許一條線程操作。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章