天天看點

【刨根問底】帶你深入了解JUC并發工具類 — 緩存一緻性和記憶體屏障

大家好,我是Java不惑(WX公衆号同名)。這是專欄的第二篇文章,我将給大家簡單介紹一下volatile和cas的原理。

為什麼說簡單介紹,因為不同的處理器有不同的實作方式,并且處理器過于複雜,我們隻需要簡單了解就可以了。

在這篇文章中,我将向大家介紹緩存一緻性協定,并介紹緩存一緻性協定是怎樣實作可見性和有序性。

lock指令字首

對volatile修飾的變量,編譯後的指令增加了lock指令的字首:

lock add1 $0x0,(%esp)
           

CAS編譯後,也會自動增加lock字首。

lock cmpxchg
           

正是加了lock字首,才讓volatile修飾符具有可見性和有序性,也讓cas可以原子的替換變量。

那麼lock指令究竟讓處理器做了什麼操作呢?

在上一篇文章中,我們知道了核心之間是可以通信的,那麼核心之間是怎麼通信的呢?

帶着疑問,我們繼續向下看。

Lock指令在處理器中有兩種處理實作方式:總線鎖和緩存一緻性協定。

總線鎖

總線鎖顧名思義,就是鎖住總線。CPU總線負責CPU和外部(高速緩存、記憶體等)通信,使用總線鎖會選擇一個核心獨占總線,其他核心不能和記憶體通信。

此時其它核心無法通信,該核心獨享共享記憶體,也就解決了原子性問題。

但是核心無法通信時,開銷比較大,在最初的處理器中提供這種方式,現在又提出了緩存一緻性協定。

緩存一緻性機制

現在處理器有專門的協定來解決多核緩存中資料一緻性問題,比較經典的有MESI協定,下面我們主要介紹MESI協定。

上一篇文章中我們介紹過緩存行,在MESI的實作中,每個緩存行包含三部分:vaild、tag和block,三部分介紹如下:

  • vaild 用于辨別該資料的有效性
  • tag 用于訓示資料對應的記憶體位址
  • block 用于存儲資料(64byte)

vaild是校驗字段,我們能通過這個字段去判斷緩存行是否是可用的。對于vaild字段有兩種處理方式:

  • 一種方式是當一個核心修改了資料,其他核心如果有這份資料,就把valid辨別為無效,這種方式叫做寫失效;
  • 一種方式是當一個核心修改了資料,其他核心如果有這份資料,就更新為新值,标記valid有效,這種方式叫做寫更新;

MESI協定使用的是寫失效的思路。在MESI協定中,緩存行valid有四種狀态:

1、 M(Modified,被修改):緩存行中的資料隻緩存在該核心的緩存中,并被修改過。目前緩存行中的資料和主存中的資料不一緻,需要在某個時間寫回到主存中。

【刨根問底】帶你深入了解JUC并發工具類 — 緩存一緻性和記憶體屏障

2、 E(Exclusive,獨享):該緩存行隻被緩存在該核心的緩存中,并且是未被修改的,與主存中的資料一緻。當其他核心讀取該緩存行時變為共享狀态。

【刨根問底】帶你深入了解JUC并發工具類 — 緩存一緻性和記憶體屏障

3、 S(Shared,共享):該緩存行可能被多個核心緩存,并且各緩存行中的資料和主資料一緻,當有一個CPU修改資料時,其他核心中該緩存行中的資料被設定為無效狀态。

【刨根問底】帶你深入了解JUC并發工具類 — 緩存一緻性和記憶體屏障

4、 I(Invalid,無效):該緩存行無效

核心通信

MESI協定要求緩存沒有命中時,允許緩存在其他緩存中複制資料,是以減少了讀取主存。

首先我們看一下核心修改資料後,會怎麼處理,緩存行中的資料會怎麼變化。

【刨根問底】帶你深入了解JUC并發工具類 — 緩存一緻性和記憶體屏障
  • 核心0修改data的資料為0,會先向所有核心廣播,告訴其他核心要修改的資料
  • 核心1接收核心0的廣播消息之後,會檢查緩存中的包含該資料的緩存行
  • 核心1将查找到的緩存行删掉或者設定為無效狀态(vaild設為I狀态)
【刨根問底】帶你深入了解JUC并發工具類 — 緩存一緻性和記憶體屏障
  • 核心0收到核心1的回報後,會将資料儲存到緩存行中,并修改valid為E或者M
  • 核心0儲存資料到記憶體中。

為了保證各個核心讀取的data共享變量是最新的,core0 需要等到core1傳回消息後才能繼續執行。

但是核心0不可能等待接收到核心1的消息之後才進行後續操作。

是以新增了一個存儲緩存的元件。

存儲緩沖(Store Buffer)

【刨根問底】帶你深入了解JUC并發工具類 — 緩存一緻性和記憶體屏障

core0廣播消息時,先将消息儲存到StoreBuffer中,這樣core0就不必知道其他核心是否已經接收到消息。

核心中引入Store Buffer解決了核心之間等待的問題,但是又引入了Store Buffer和Cache之間資料同步的問題。

為此,針對Store Buffer,CPU在後續變量新值寫入之前,把Store Buffer的所有值按順序重新整理到記憶體中。

這就稱為記憶體屏障中的寫屏障(Store Barrier)

無效隊列(Invalidate Queue)

core0廣播資料修改之後,core1不可能馬上處理,而是在核心中新增一個無效隊列的元件,用于存放接收廣播中的無效資料。

【刨根問底】帶你深入了解JUC并發工具類 — 緩存一緻性和記憶體屏障
  • 當其他核心收到無效指令時,不需要确認緩存行是否真正失效,而是先放到Invalidate Queue中,并傳回無效指令确認;等待核心空閑時再處理Invalidate Queue中的無效指令。
  • Invalidate Queue的引入解決了核心不能及時回複消息的問題,但也帶來了一些問題,比如未及時将緩存行設定為無效狀态并使用了該緩存行。
  • 為此,針對Invalidate Queue,執行後需要等待Invalidate Queue完全應用到記憶體後,後續讀操作才繼續執行,保證執行前後的讀操作對其他核心是順序的,這也稱為記憶體屏障中的讀屏障(Load Barrier)。

volatile和緩存一緻性協定

上面我們介紹了總線鎖和緩存一緻性,使用lock字首的指令,核心會使用緩存一緻性協定來處理共享資料。

記憶體屏障有讀屏障、寫屏障和全屏障(full barrier)等幾種。

讀屏障:擷取其他核心修改,讓目前核心中的資料為最新的值,也就是将Invalidate Queue中的資料應用到核心;

寫屏障:将核心的修改讓其他核心可見,将storeBuffer的資料寫入緩存/記憶體中;

全屏障:是幾種記憶體屏障裡面開銷最大的,包含了其他幾種屏障;

因為有緩存一緻性和總線鎖,是以volatile實作了可見性。

同時lock具有full barrier的效果,是以volatile保證了有序性。

總結

在這篇文章中,我介紹了緩存一緻性和volatile的關系,以及volatile是怎樣實作可見性和有序性的。希望你看完有所收獲,受限于個人水準,文章若有錯漏,還望讀者不吝賜教。

在下一篇文章中,我将向大家介紹Synchronized和Reentrantlock背後的設計思想,也就是大學教材《計算機作業系統》中的信号量和管程。

最後,如果我的文章對你有幫助,請幫我點贊轉發!

如果你對volatile和cas不熟悉,可以看我的第一篇文章《【刨根問底】帶你深入了解JUC并發工具類 — volatile和cas》

或者直接通路該專欄的導航文章:《【刨根問底】帶你深入了解JUC并發工具類 — 開篇》