Java編程偽共享與緩存行填充
最近在回顧Disruptor
的相關(guān)知識,覺得Disruptor
在計算機底層的領(lǐng)域確實比一般人厲害不少,以前在寫程序的時候,基本是從應(yīng)用邏輯的角度考慮,覺得設(shè)計模式+少量算法+ 優(yōu)美的代碼=理想的結(jié)果,但看完Disruptor
的設(shè)計后,覺得只考慮應(yīng)用本身是有一定的局限性,還需要懂底層,硬件層面的東西,就像Disruptor
一樣,通過底層優(yōu)化,讓程序有質(zhì)的飛躍。
下面就Disruptor
提到的CPU緩存話題,做了一些嘗試和研究,如Disruptor
所說,CPU有緩存?zhèn)喂蚕淼膯栴},并且通過緩存行填充能完美的解決這個問題。
1、CPU緩存
CPU是機器的心臟,最終由它來執(zhí)行所有運算和程序。主內(nèi)存(RAM)是存放數(shù)據(jù)(包括代碼行)的地方。CPU和主內(nèi)存之間有好幾層緩存,即使直接訪問主內(nèi)存也是非常慢的。如果你正在多次對一塊數(shù)據(jù)做相同的運算,那么在執(zhí)行運算的時候把它加載到離CPU很近的地方就有意義了(比如一個循環(huán)計數(shù))。下面是CPU的緩存結(jié)構(gòu)圖:
越靠近CPU的核緩存越快但是也越小,所以一級緩存很小但很快,并且緊靠著在使用它的CPU內(nèi)核。二級緩存大一些,也慢一些,注意一級二級緩存只能被一個單獨的CPU的單個核使用。三級緩存在現(xiàn)代多核機器中更普遍,仍然更大,更慢,但是被單個插槽上的所有CPU核共享。最后,你擁有一塊主存,由全部插槽上的所有CPU核共享。當(dāng)CPU執(zhí)行運算的時候,它先去一級緩存查找所需的數(shù)據(jù),再去二級緩存,然后是三級緩存,最后如果這些緩存中都沒有,所需的數(shù)據(jù)就要去主內(nèi)存拿。走得越遠(yuǎn),運算耗費的時間就越長。所以如果你在做一些很頻繁的事,你要確保數(shù)據(jù)在一級緩存中。
這是在網(wǎng)上找到的一份CPU緩存未命中時候的CPU時鐘消耗一級大概的耗時:
2、CPU緩存行與偽共享
數(shù)據(jù)在緩存中不是以獨立的項來存儲,不是單獨的變量,也不是單獨的指針。緩存系統(tǒng)中是以緩存行(cache line
)為單位存儲,緩存行是2的整數(shù)冪個連續(xù)字節(jié),一般為32-256
個字節(jié),最常見的緩存行大小是64個字節(jié)。
下面是CPU緩存行的邏輯圖:
CPU從主內(nèi)存中加載數(shù)據(jù)的時候,不是只加載某一個變量的值,而是加載一個緩存行的值,例如一個Java的long類型是8字節(jié),因此在一個緩存行中可以存8個long類型的變量。如果你訪問一個long
類型的數(shù)組,當(dāng)數(shù)組中的一個值被加載到緩存中,它會額外加載另外7個。如果你數(shù)據(jù)結(jié)構(gòu)中的項在內(nèi)存中不是彼此相鄰的,例如鏈表LinkedList
結(jié)構(gòu),你將得不到緩存行加載所帶來的優(yōu)勢,并且在這些數(shù)據(jù)結(jié)構(gòu)中的每一個項都可能會出現(xiàn)緩存未命中,這是也是鏈表不適合遍歷的原因之一。
但是,緩存行加載某一塊內(nèi)存數(shù)據(jù),這個有好處也有壞處,緩存行不是單個數(shù)據(jù),而是一組數(shù)據(jù),如上圖所示當(dāng)2個線程同時運行在2個core
上,同時加載了同一個緩存行,Core1
修改X數(shù)據(jù),Core2
讀Y數(shù)據(jù),Core1
修改后提交,Core2
發(fā)現(xiàn)X數(shù)據(jù)有變化,緩存未命中,就會重新加載整個緩存行,但是Core2
并不會用X數(shù)據(jù),而是讀Y數(shù)據(jù),去重新加載整個緩存行的數(shù)據(jù),無意中影響彼此的性能。如果兩個獨立的線程同時寫兩個不同的值會更糟,因為每次線程對緩存行進行寫操作時,每個內(nèi)核都要把另一個內(nèi)核上的緩存塊無效掉并重新讀取里面的數(shù)據(jù)。你基本上是遇到兩個線程之間的寫沖突了,盡管它們寫入的是不同的變量。每個線程都要去競爭緩存行的所有權(quán)來更新變量。如果核心1獲得了所有權(quán),緩存子系統(tǒng)將會使核心2中對應(yīng)的緩存行失效。當(dāng)核心2獲得了所有權(quán)然后執(zhí)行更新操作,核心1就要使自己對應(yīng)的緩存行失效。這會來來回回的經(jīng)過CPU三級緩存,大大影響了性能。如果互相競爭的核心位于不同的插槽,就要額外橫跨插槽連接,問題可能更加嚴(yán)重,這就是CPU緩存?zhèn)喂蚕怼?/p>
3、Java處理緩存?zhèn)喂蚕?/h2>
緩存行填充:
因為是硬件底層的邏輯,幾乎所有程序在跑的時候都會遇到這個問題,那么java是如何處理這個問題呢?答案就是 緩存行填充 。
對于HotSpot JVM
,所有對象都有兩個字長的對象頭。第一個字是由24位哈希碼和8位標(biāo)志位(如鎖的狀態(tài)或作為鎖對象)組成的Mark Word。第二個字是對象所屬類的引用。如果是數(shù)組對象還需要一個額外的字來存儲數(shù)組的長度。每個對象的起始地址都對齊于8字節(jié)以提高性能。因此當(dāng)封裝對象的時候為了高效率,對象字段聲明的順序會被重排序成下列基于字節(jié)大小的順序:
doubles (8) 和 longs (8) ints (4) 和 floats (4) shorts (2) 和 chars (2) booleans (1) 和 bytes (1) references (4/8) <子類字段重復(fù)上述順序>
通過對熱點變量周圍進行緩存行填充,來規(guī)避緩存?zhèn)喂蚕韼淼膯栴},對于緩存行大小是64字節(jié)或更少的處理器架構(gòu)來說是這樣的,有可能處理器的緩存行是128字節(jié),那么使用64字節(jié)填充還是會存在偽共享問題,通過增加補全變量的個數(shù)來確保熱點變量不會和其他東西同時存在于一個緩存行中。下面是Disruptor
對ring buffer
的序列號做的補全代碼:
public long p1, p2, p3, p4, p5, p6, p7; // cache line padding private volatile long cursor = INITIAL_CURSOR_VALUE; public long p8, p9, p10, p11, p12, p13, p14; // cache line padding
當(dāng)CPU緩存加載cursor
變量的時候,會連帶加載周邊的7個long
類型變量,但是這幾個long
類型變量不會有任何線程去修改它,因此不會出現(xiàn)緩存未命中問題,完美規(guī)避了緩存?zhèn)喂蚕淼膯栴}。
4、Java程序代碼驗證
官方也給了一個java
的測試demo
,那么下面針對各種不同的情景,做一下實驗看看,是不是有緩存?zhèn)喂蚕磉@個問題,測試代碼如下:
下面針對各個測試場景,做一下簡單的描述:
場景一:對Long
變量進行寫入,沒有緩存行填充,沒有volatile
關(guān)鍵字。
場景二:對Long
變量進行寫入,有緩存行填充,沒有volatile
關(guān)鍵字。
場景三:對Long
變量進行寫入,沒有緩存行填充,有volatile
關(guān)鍵字。
場景四:對Long
變量進行寫入,有緩存行填充,有volatile
關(guān)鍵字。
下面是針對各個場景的測試結(jié)果(每個場景測試3次,取平均值):
從測試結(jié)果來看,場景一和場景二差不多,有緩存行填充的稍微快那么一點點,區(qū)別不大,都是192276000
納秒左右。場景三和場景四有volatile
關(guān)鍵字的就不一樣了,這里可以看出volatile
關(guān)鍵字對一個變量的讀取和寫入性能影響還是比較大,寫入耗時是直接寫入的200多倍,因此volatile
關(guān)鍵字怎么用很關(guān)鍵,用到哪些地方也很關(guān)鍵,不要在代碼里面隨便加,不會用反而會影響程序運行效率。場景三有volatile
關(guān)鍵字,但是沒有進行緩存行填充,耗時是有緩存行填充的10幾倍,這里就能看出緩存行填充的效果在用到了內(nèi)存屏障的時候還是很明顯。
CPU緩存?zhèn)喂蚕淼膯栴},確實打破了很多人對常規(guī)程序執(zhí)行的理解,如何才能應(yīng)用到工作中呢?有以下幾點需要注意:
- 對
volatile
很熟悉,并且代碼里面使用到了緩存屏障,需要看看能否用到這個緩存填充行。 - 清楚程序在某個時刻會有緩存?zhèn)喂蚕韱栴},例如某幾個代碼在一起的變量會被多個線程同時使用并且有寫入操作,需要用緩存填充行把這幾個變量隔開。
- 能使用工具分析自己寫的程序,看看有緩存填充行過后,是否真的能提升效率,例如
JProfiler
分析工具。
到此這篇關(guān)于Java編程偽共享與緩存行填充的文章就介紹到這了,更多相關(guān)Java編程偽共享與緩存行填充內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring使用ThreadPoolTaskExecutor自定義線程池及異步調(diào)用方式
這篇文章主要介紹了Spring使用ThreadPoolTaskExecutor自定義線程池及異步調(diào)用方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-02-02MyBatis-Plus updateById不更新null值的方法解決
用Mybatis-Plus的updateById()來更新數(shù)據(jù)時,無法將字段設(shè)置為null值,更新后數(shù)據(jù)還是原來的值,本文就來詳細(xì)的介紹一下解決方法,具有一定的參考價值,感興趣的可以了解一下2023-08-08詳解BeanUtils.copyProperties()方法如何使用
這篇文章主要為大家介紹了詳解BeanUtils.copyProperties()方法如何使用,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-07-07