Java內存屏障詳解
為什么要有內存屏障
為了解決cpu,高速緩存,主內存帶來的的指令之間的可見性和重序性問題。
我們都知道計算機運算任務需要CPU和內存相互配合共同完成,其中CPU負責邏輯計算,內存負責數(shù)據(jù)存儲。CPU要與內存進行交互,如讀取運算數(shù)據(jù)、存儲運算結果等。由于內存和CPU的計算速度有幾個數(shù)量級的差距,為了提高CPU的利用率,現(xiàn)代處理器結構都加入了一層讀寫速度盡可能接近CPU運算速度的高速緩存來作為內存與CPU之間的緩沖:將運算需要使用
的數(shù)據(jù)復制到緩存中,讓CPU運算可以快速進行,計算結束后再將計算結果從緩存同步到主內存中,這樣處理器就無須等待緩慢的內存讀寫了。
什么是內存屏障
內存屏障,也稱內存柵欄,內存柵障,屏障指令等, 是一類同步屏障指令,是CPU或編譯器在對內存隨機訪問的操作中的一個同步點,使得此點之前的所有讀寫操作都執(zhí)行后才可以開始執(zhí)行此點之后的操作。
程序編譯優(yōu)化、cache訪問優(yōu)化、多核等導致CPU指令亂序執(zhí)行,最終程序運行不符合我們預期。內存屏障會設置一個同步點,保障屏障前后的多核內存訪問數(shù)據(jù)的一致性。
問題的由來
造成亂序訪問的原因分為兩類:
一類是主動的,編譯器會主動重排代碼使得特定的cpu執(zhí)行更快,稱之為編譯亂序。
另外一類是被動的,為了異步化指令的執(zhí)行,引入Store Buffer和Invalidate Queue,卻導致了指令順序改變的副作用。
1)指令重排序
上述的1屬于編譯器重排序,2和3屬于處理器重排序。這些重排序都可能會導致多線程程序出現(xiàn)內存可見性問題。
2)store buffer
加入了這個硬件結構后,CPU0需要往某個地址中寫入一個數(shù)據(jù)時,它不需要去關心其他的CPU的local cache中有沒有這個地址的數(shù)據(jù),它只需要把它需要寫的值直接存放到store buffer中,然后發(fā)出invalidate的信號,等到成功invalidate其他CPU中該地址的數(shù)據(jù)后,再把CPU0存放在store buffer中的數(shù)據(jù)推到CPU0的local cache中。每一個CPU core都擁有自己私有的store buffer,一個CPU只能訪問自己私有的那個store buffer。
該硬件同時也有缺陷,每個CPU的store buffer不能實現(xiàn)地太大,其存儲隊列的數(shù)目也不會太多。當CPU以中等的頻率執(zhí)行store操作的時候(假設所有的store操作都導致了cache miss),store buffer會很快的唄填滿。在這種情況下,CPU只能又進入阻塞狀態(tài),直到cacheline完成invalidation和ack的交互后,可以將store buffer的entry寫入cacheline,從而讓新的store讓出空間之后,CPU才可以繼續(xù)被執(zhí)行。
3)Invalidate Queues
store buffer之所以很容易被填滿,主要是因為其他CPU在回應invalidate acknowledge比較慢,如果能加快這個過程,讓store buffer中的內容盡快寫入到cacheline,那么就不會那么容易被填滿了。
CPU其實不需要完成invalidate就可以回送acknowledgement消息,這樣就不會阻止發(fā)送invalidate的那個CPU進去阻塞狀態(tài)。
CPU可以將這些接收到的invalidate message存放到invalidate queues中,然后直接回應acknowledge,表示自己已經(jīng)收到請求,隨后會慢慢處理,當時前提是必須在發(fā)送invalidate message的CPU發(fā)送任何關于某變量對應cacheline的操作到bus之前完成。
4)亂序處理器
類比工業(yè)流水線,一條指令的執(zhí)行可以分拆為多步:獲取、解碼、運算和結果的寫入,每個步驟由一個特定的功能模塊執(zhí)行,如此拆分的好處是多條執(zhí)行變串行執(zhí)行為并行執(zhí)行
5)什么場景下需要使用內存屏障
在兩個線程之間存在需要通過共享內存來實現(xiàn)交互的可能時,才需要使用內存屏障,,保證共享變量的可見性。
內存屏障指令
處理器重排序類型
下面是常見處理器允許的重排序類型的列表:
處理器 | Load-Load | Load-Store | Store-Store | Store-Load | 數(shù)據(jù)依賴 |
spare-TSO | N | N | N | Y | N |
X86 | N | N | N | Y | N |
ia64 | Y | Y | Y | Y | N |
PowerPC | Y | Y | Y | Y | N |
上表單元格中的“N”表示處理器不允許兩個操作重排序,“Y”表示允許重排序。
從上表我們可以看出:
1)常見的處理器都允許Store-Load重排序;
2)常見的處理器都不允許對存在數(shù)據(jù)依賴的操作做重排序。sparc-TSO和x86擁有相對較強的處理器內存模型,它們僅允許對寫-讀操作做重排序(因為它們都使用了寫緩沖區(qū))。
JVM內存屏障指令分類
屏障類型 | 指令示例 | 說明 |
LoadLoadBarriers | Load1;LoadLoad;Load2; | 確保Load1的數(shù)據(jù)加載,前于Load2及所有后續(xù)裝載指令的裝著 |
StoreStoreBarriers | Store1;StoreStore;Store2; | 確保Store1數(shù)據(jù)對其他處理器可見(刷新到內存),前于Store2及所有后續(xù)存儲指令的存儲。 |
LoadStoreBarriers | Load1;LoadStore;Store2; | 確保Load1數(shù)據(jù)裝載,前于Store2及所有后續(xù)的存儲指令刷新到內存。 |
StoreLoadBarriers | Store1;StoreLoad;Load2; | 確保Store1數(shù)據(jù)對其他處理器變得可見(指刷新到內存),之前于Load2及所有后續(xù)裝載指令的裝載。StoreLoad Barriers會使該屏障之前的所有內存訪問指令(存儲和裝載指令)完成之后,才執(zhí)行該屏障之后的內存訪問指令。 |
StoreLoad Barriers是一個“全能型”的屏障,它同時具有其他三個屏障的效果。
現(xiàn)代的多處理器大都支持該屏障(其他類型的屏障不一定被所有處理器支持)。
執(zhí)行該屏障開銷會很昂貴,因為當前處理器通常要把寫緩沖區(qū)中的數(shù)據(jù)全部刷新到內存中(buffer fully flush)。
總結
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
Java Validation Api實現(xiàn)原理解析
這篇文章主要介紹了Java Validation Api實現(xiàn)原理解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-09-09Mybatis-Plus通過配置在控制臺打印執(zhí)行日志的實現(xiàn)
本文主要介紹了Mybatis-Plus通過配置在控制臺打印執(zhí)行日志的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2025-04-04