內(nèi)存屏障由來及實(shí)現(xiàn)思路
很多人知道內(nèi)存屏障這個(gè)東西應(yīng)該是在學(xué)習(xí)volatile時(shí)看到的,但是對(duì)內(nèi)存屏障依然存在很多疑惑:為什么要加內(nèi)存屏障?內(nèi)存屏障能解決什么問題?為什么能解決這些問題?
最近在研究volatile的過程中發(fā)現(xiàn)內(nèi)存屏障這東西如果不搞明白,Java中的volatile就別想學(xué)透,所以花了較長時(shí)間來研究這塊??戳撕芏噘Y料,寫了很多代碼測(cè)試,這篇文章就來總結(jié)下我目前認(rèn)知中的內(nèi)存屏障。
01 CPU緩存
如果你不了解講內(nèi)存屏障為什么要講CPU緩存,接著往后看。
學(xué)過《計(jì)算機(jī)組成原理》的同學(xué)應(yīng)該都聽過一個(gè)詞:時(shí)鐘周期。什么是時(shí)鐘周期呢?通俗點(diǎn)來講就是CPU完成一個(gè)基本動(dòng)作需要的時(shí)間周期。對(duì)硬件有點(diǎn)認(rèn)識(shí)的同學(xué)都知道看CPU好不好一定要看的一個(gè)參數(shù):多少多少GHZ。這個(gè)GHZ跟時(shí)鐘周期之間是存在一定的換算關(guān)系的,感興趣的同學(xué)可以去自行研究。說明一下:不了解這層換算關(guān)系不影響你看后面的內(nèi)容,只要你對(duì)時(shí)鐘周期有一個(gè)基本認(rèn)知就可以了。
在很早以前,CPU里面是沒有緩存這塊區(qū)域的,就是CPU直接讀寫內(nèi)存。那后面為什么在CPU中增加了緩存呢?因?yàn)镃PU的運(yùn)行效率與讀寫內(nèi)存的效率存在著巨大的鴻溝,在讀寫內(nèi)存過程中帶來的等待浪費(fèi)了很大的CPU算力?,F(xiàn)在最新的內(nèi)存是DDR4規(guī)格,但是向內(nèi)存中寫入數(shù)據(jù),據(jù)權(quán)威資料,需要107個(gè)CPU時(shí)鐘周期,即CPU的運(yùn)行效率是寫內(nèi)存的107倍。如果CPU只執(zhí)行寫操作需要一個(gè)時(shí)鐘周期,那CPU等待這個(gè)寫完成需要等待106個(gè)時(shí)鐘周期,是不是很浪費(fèi)CPU算力?那如何解決呢?就跟我們工作中發(fā)現(xiàn)MySQL出現(xiàn)讀寫瓶頸如何解決是一樣的思維:加緩存。
拿我們今天主流的CPU架構(gòu)來說,現(xiàn)在的CPU主要采用三層緩存:
L1、L2緩存成為本地核心內(nèi)緩存,即一個(gè)核一個(gè)。如果你的機(jī)器是4核,那就是有4個(gè)L1+4個(gè)L2
L3緩存是所有核共享的。即不管你的CPU是幾核,這個(gè)CPU中只有一個(gè)L3
L1緩存的大小是64K,即32K指令緩存+32K數(shù)據(jù)緩存。L2是256K,L3是2M。這不是絕對(duì)的,目前Intel CPU基本是這樣的設(shè)計(jì)
這里還補(bǔ)充一個(gè)知識(shí)點(diǎn):緩存行(Cache-line)。緩存行是CPU緩存存儲(chǔ)數(shù)據(jù)的最小單位,大小為64B。這塊如果展開來講要講很久很久,本篇文章就不展開講了,有興趣的同學(xué)可以自行研究。如果你沒有學(xué)習(xí)過計(jì)算機(jī)硬件相關(guān)知識(shí),可能看不懂。
根據(jù)哲學(xué)的矛盾相對(duì)論:任何問題的解決方案都是一個(gè)利與弊共存的矛盾體。加緩存的確有效提升了CPU的執(zhí)行效率,但是CPU緩存間的數(shù)據(jù)一致性、CPU緩存與內(nèi)存間的數(shù)據(jù)一致性就是不得不去思考與解決的問題了。而且還得保證解決這兩層數(shù)據(jù)一致性的效率要高于不加緩存前浪費(fèi)的CPU算力,不然這個(gè)方案就是一套偽方案:聽起來高大上,不解決問題。
02 緩存的一致性
MESI協(xié)議就是為了保證CPU各核的緩存、內(nèi)存間的數(shù)據(jù)一致性而生的,沒有了解過的可以百度普及一下,這個(gè)比較簡單。這里拓展兩點(diǎn):
一、CPU運(yùn)算單元與L1緩存間為什么要增加buffer?CPU實(shí)現(xiàn)各個(gè)核的緩存與內(nèi)存間的數(shù)據(jù)一致性的思路有點(diǎn)像socket的三次握手:CPU0修改了某個(gè)數(shù)據(jù),需要廣播告訴其他CPU,這時(shí)候CPU0進(jìn)入阻塞狀態(tài)等待其他CPU修改其緩存中的狀態(tài),待其他CPU都修改完?duì)顟B(tài)返回應(yīng)答消息后才進(jìn)入運(yùn)行狀態(tài)。雖然這個(gè)阻塞的時(shí)間很短,但是在CPU的世界里就很長了,為了保證這部分阻塞時(shí)間也能得到充分利用,于是加入了buffer。將預(yù)讀信息存儲(chǔ)進(jìn)去,這樣CPU解除阻塞后就可以直接從buffer拿出請(qǐng)求處理。
二、MESI協(xié)議的實(shí)現(xiàn)思路是:如果CPU0修改了某個(gè)數(shù)據(jù),需要廣播給其他CPU,緩存中沒有這個(gè)數(shù)據(jù)的CPU丟棄這個(gè)廣播消息,緩存中有這個(gè)數(shù)據(jù)的CPU監(jiān)聽到這個(gè)廣播后會(huì)將相應(yīng)的緩存行改為invalid狀態(tài),這樣CPU在下次讀取這個(gè)數(shù)據(jù)的時(shí)候發(fā)現(xiàn)緩存行失效,就去內(nèi)存中讀取。這里面童鞋們有沒有發(fā)現(xiàn)一個(gè)問題:只要存在數(shù)據(jù)修改,CPU就需要去內(nèi)存取數(shù)據(jù),那為什么不實(shí)現(xiàn)CPU緩存能共享數(shù)據(jù)呢?這樣CPU在下次讀取的時(shí)候去CPU0的緩存行去讀取就可以啦,而且性能更高。現(xiàn)在的CPU也的確實(shí)現(xiàn)了這個(gè)思路,對(duì)應(yīng)的協(xié)議就是:AMD的MOESI、Intel的MESIF。感興趣的童鞋自己去研究吧。
03 內(nèi)存屏障的由來
對(duì)于CPU的寫,目前主流策略有兩種:
1、write back:即CPU向內(nèi)存寫數(shù)據(jù)時(shí),先把真實(shí)數(shù)據(jù)放入store buffer中,待到某個(gè)合適的時(shí)間點(diǎn),CPU才會(huì)將store buffer中的數(shù)據(jù)刷到內(nèi)存中,而且這兩個(gè)操作是異步的。這在多線程環(huán)境中,有些情況下是可以接受的,但是有些情況是不可接受的,為了讓程序員有能力根據(jù)業(yè)務(wù)需要達(dá)到同步完成,就設(shè)計(jì)了內(nèi)存屏障。關(guān)于內(nèi)存屏障,后面會(huì)細(xì)講。
2、write through:即CPU向內(nèi)存寫數(shù)據(jù)時(shí),同步完成寫store buffer與內(nèi)存。
當(dāng)前CPU大多數(shù)采用的是write back策略??赡苡型獑柫耍簽槭裁茨兀恳?yàn)榇蠖鄶?shù)情況下,CPU異步完成寫內(nèi)存產(chǎn)生的部分延遲是可以接受的,而且這個(gè)延遲極短。只有在多線程環(huán)境下需要嚴(yán)格保證內(nèi)存可見等極少數(shù)特殊情況下才需要保證CPU的寫在外界看來是同步完成的,需要借助CPU提供的內(nèi)存屏障實(shí)現(xiàn)。如果直接采用策略2:write through,那每次寫內(nèi)存都需要等待數(shù)據(jù)刷入內(nèi)存,極大影響了CPU的執(zhí)行效率。
04 內(nèi)存屏障實(shí)現(xiàn)思路
為什么要插入屏障?本質(zhì)是業(yè)務(wù)層面不能接受寫store buffer與刷回內(nèi)存這兩個(gè)異步操作產(chǎn)生的哪怕是極少的延遲,即對(duì)內(nèi)存可見性的要求極高。
內(nèi)存屏障到底是什么?內(nèi)存屏障什么都不是,它只是一個(gè)抽象概念,就像OOP。如果這樣說你不理解,那你把他理解成一堵墻,這堵墻正面與反面的指令無法被CPU亂序執(zhí)行及這堵墻正面與反面的讀寫操作需有序執(zhí)行。
CPU提供了三個(gè)匯編指令串行化運(yùn)行讀寫指令達(dá)到實(shí)現(xiàn)保證讀寫有序性的目的:
SFENCE
:在該指令前的寫操作必須在該指令后的寫操作前完成
LFENCE
:在該指令前的讀操作必須在該指令后的讀操作前完成
MFENCE
:在該指令前的讀寫操作必須在該指令后的讀寫操作前完成
何謂串行化?你可以理解成CPU把讀、寫、讀寫請(qǐng)求放入了一個(gè)隊(duì)列,按照先進(jìn)先出的順序執(zhí)行下去;何謂讀操作完成,即CPU執(zhí)行一次讀操作,把值讀到了寄存器中;何謂寫操作完成,即CPU執(zhí)行一次寫操作,數(shù)據(jù)刷到內(nèi)存中了。
到這里就把內(nèi)存屏障講清楚了,你悟了嗎?
以上就是內(nèi)存屏障由來及實(shí)現(xiàn)思路的詳細(xì)內(nèi)容,更多關(guān)于內(nèi)存屏障的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java中如何將?int[]?數(shù)組轉(zhuǎn)換為?ArrayList(list)
這篇文章主要介紹了Java中將?int[]?數(shù)組?轉(zhuǎn)換為?List(ArrayList),本文通過示例代碼給大家講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-12-12Struts2學(xué)習(xí)筆記(4)-通配符的使用
本文主要介紹Struts2中通配符的使用,簡單實(shí)用,希望能給大家做一個(gè)參考。2016-06-06Spring Boot 項(xiàng)目啟動(dòng)自動(dòng)執(zhí)行方法的兩種實(shí)現(xiàn)方式
這篇文章主要介紹了Spring Boot 項(xiàng)目啟動(dòng)自動(dòng)執(zhí)行方法的兩種實(shí)現(xiàn)方式,幫助大家更好的理解和學(xué)習(xí)使用Spring Boot框架,感興趣的朋友可以了解下2021-05-05使用IDEA異常斷點(diǎn)來定位java.lang.ArrayStoreException的問題
這篇文章主要介紹了使用IDEA異常斷點(diǎn)來定位java.lang.ArrayStoreException的問題,平常開發(fā)過程中面對(duì)這種描述不夠清楚,無法定位具體原因的問題該如何處理,下面我們來一起學(xué)習(xí)一下吧2019-06-06詳解Spring-boot中讀取config配置文件的兩種方式
這篇文章主要介紹了詳解Spring-boot中讀取config配置文件的兩種方式,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-10-10java中對(duì)Redis的緩存進(jìn)行操作的示例代碼
本篇文章主要介紹了java中對(duì)Redis的緩存進(jìn)行操作的示例代碼,具有一定的參考價(jià)值,有興趣的可以了解一下2017-08-08JDK1.7 Paths,Files類實(shí)現(xiàn)文件夾的復(fù)制與刪除的實(shí)例
下面小編就為大家分享一篇JDK1.7 Paths,Files類實(shí)現(xiàn)文件夾的復(fù)制與刪除的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。以前跟隨小編過來看看吧2017-11-11剖析Java中在Collection集合中使用contains和remove為什么要重寫equals
這篇文章主要介紹了Collection集合的contains和remove方法詳解remove以及相關(guān)的經(jīng)驗(yàn)技巧,通過簡要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-09-09