大家好,我是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,被修改):緩存行中的資料隻緩存在該核心的緩存中,并被修改過。目前緩存行中的資料和主存中的資料不一緻,需要在某個時間寫回到主存中。
2、 E(Exclusive,獨享):該緩存行隻被緩存在該核心的緩存中,并且是未被修改的,與主存中的資料一緻。當其他核心讀取該緩存行時變為共享狀态。
3、 S(Shared,共享):該緩存行可能被多個核心緩存,并且各緩存行中的資料和主資料一緻,當有一個CPU修改資料時,其他核心中該緩存行中的資料被設定為無效狀态。
4、 I(Invalid,無效):該緩存行無效
核心通信
MESI協定要求緩存沒有命中時,允許緩存在其他緩存中複制資料,是以減少了讀取主存。
首先我們看一下核心修改資料後,會怎麼處理,緩存行中的資料會怎麼變化。
- 核心0修改data的資料為0,會先向所有核心廣播,告訴其他核心要修改的資料
- 核心1接收核心0的廣播消息之後,會檢查緩存中的包含該資料的緩存行
- 核心1将查找到的緩存行删掉或者設定為無效狀态(vaild設為I狀态)
- 核心0收到核心1的回報後,會将資料儲存到緩存行中,并修改valid為E或者M
- 核心0儲存資料到記憶體中。
為了保證各個核心讀取的data共享變量是最新的,core0 需要等到core1傳回消息後才能繼續執行。
但是核心0不可能等待接收到核心1的消息之後才進行後續操作。
是以新增了一個存儲緩存的元件。
存儲緩沖(Store Buffer)
core0廣播消息時,先将消息儲存到StoreBuffer中,這樣core0就不必知道其他核心是否已經接收到消息。
核心中引入Store Buffer解決了核心之間等待的問題,但是又引入了Store Buffer和Cache之間資料同步的問題。
為此,針對Store Buffer,CPU在後續變量新值寫入之前,把Store Buffer的所有值按順序重新整理到記憶體中。
這就稱為記憶體屏障中的寫屏障(Store Barrier)
無效隊列(Invalidate Queue)
core0廣播資料修改之後,core1不可能馬上處理,而是在核心中新增一個無效隊列的元件,用于存放接收廣播中的無效資料。
- 當其他核心收到無效指令時,不需要确認緩存行是否真正失效,而是先放到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并發工具類 — 開篇》